Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e3120d8
trying out stuff
nipunh Mar 16, 2025
51f1a78
basic implementation completed
nipunh Mar 16, 2025
e9060fd
snapshots page completed
nipunh Mar 18, 2025
97d266a
test cases added
nipunh Mar 18, 2025
1179ff6
Merge branch 'main' into feature/communit-snapshots-page
nipunh Mar 18, 2025
391e523
improved code quality
nipunh Mar 18, 2025
03c09b5
corrections
nipunh Mar 18, 2025
6c57eb8
mobile navigation implemented
nipunh Mar 18, 2025
c073c2a
code smell removed
nipunh Mar 18, 2025
d0eb718
code smell removed
nipunh Mar 18, 2025
f59dffd
formatting issue
nipunh Mar 18, 2025
9c69cec
removed trailing spaces
nipunh Mar 18, 2025
33657ea
code formatting improved
nipunh Mar 18, 2025
658de49
code formatting improved
nipunh Mar 18, 2025
4fd3010
test cases updated
nipunh Mar 18, 2025
e0e7f46
Merge branch 'main' into feature/communit-snapshots-page
arkid15r Mar 18, 2025
1aab7ad
updated branch
nipunh Mar 20, 2025
64cfe7c
Merge branch 'main' into feature/communit-snapshots-page
nipunh Mar 20, 2025
81f3fbc
Merge branch 'main' into feature/communit-snapshots-page
arkid15r Mar 24, 2025
89af8c7
Merge branch 'main' into feature/communit-snapshots-page
nipunh Mar 28, 2025
3c79828
Merge branch 'main' into feature/communit-snapshots-page
arkid15r Mar 29, 2025
65f4c4f
test cases updated
nipunh Mar 29, 2025
975d70d
code structure updated
nipunh Mar 29, 2025
4eac7b5
updated few cases
nipunh Mar 29, 2025
d2aed1a
test coverage completed
nipunh Mar 29, 2025
c596951
code cleanup
nipunh Mar 29, 2025
0ec9351
nitpicks resolved
nipunh Mar 29, 2025
e164b45
formating
nipunh Mar 29, 2025
342df6d
updated test case
nipunh Mar 29, 2025
af3a9e5
Merge branch 'main' into pr/nipunh/1130
arkid15r Mar 30, 2025
5bdf0bc
Update code
arkid15r Mar 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/apps/owasp/graphql/queries/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class SnapshotQuery(BaseQuery):
key=graphene.String(required=True),
)

recent_snapshots = graphene.List(
snapshots = graphene.List(
SnapshotNode,
limit=graphene.Int(default_value=8),
)
Expand All @@ -27,6 +27,6 @@ def resolve_snapshot(root, info, key):
except Snapshot.DoesNotExist:
return None

def resolve_recent_snapshots(root, info, limit):
def resolve_snapshots(root, info, limit):
"""Resolve recent snapshots."""
return Snapshot.objects.order_by("-created_at")[:limit]
109 changes: 109 additions & 0 deletions frontend/__tests__/unit/pages/Snapshots.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useQuery } from '@apollo/client'
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
import { mockSnapshotDetailsData } from '@unit/data/mockSnapshotData'
import { useNavigate } from 'react-router-dom'
import { toaster } from 'components/ui/toaster'
import Snapshots from 'pages/Snapshots'

jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'),
useQuery: jest.fn(),
}))

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
}))

jest.mock('components/ui/toaster', () => ({
toaster: {
create: jest.fn(),
__esModule: true,
default: jest.fn(),
},
}))

describe('Snapshots Component', () => {
afterEach(() => {
jest.clearAllMocks()
})

test('renders loading spinner initially', async () => {
// Mocking the return value of useQuery
;(useQuery as jest.Mock).mockReturnValue({ data: null, loading: true, error: null })
render(<Snapshots />)

expect(screen.getByRole('status')).toBeInTheDocument()
})

test('renders snapshots after data is loaded', async () => {
// Mocking the return value with snapshot data
;(useQuery as jest.Mock).mockReturnValue({
data: { snapshots: mockSnapshotDetailsData },
loading: false,
error: null,
})
render(<Snapshots />)

await waitFor(() => {
expect(screen.getByText('Snapshot 1')).toBeInTheDocument()
expect(screen.getByText('Snapshot 2')).toBeInTheDocument()
})
})

test('displays error message on GraphQL error', async () => {
// Mocking the return value with an error
(useQuery as jest.Mock).mockReturnValue({ data: null, loading: false, error: new Error('GraphQL Error') });
render(<Snapshots />);

await waitFor(() => {
expect(toaster).toHaveBeenCalledWith(
expect.objectContaining({ title: 'GraphQL Request Failed' })
);
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

test('navigates to snapshot details on button click', async () => {
const navigateMock = jest.fn();
(useNavigate as jest.Mock)(navigateMock);
// Mocking the return value with snapshot data
(useQuery as jest.Mock).mockReturnValue({ data: { snapshots: mockSnapshotDetailsData }, loading: false, error: null });
render(<Snapshots />);

await waitFor(() => {
const viewButtons = screen.getAllByText('View Details');
expect(viewButtons).toHaveLength(2);
fireEvent.click(viewButtons[0]);
});

expect(navigateMock).toHaveBeenCalledWith('/community/snapshots/1');
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

test('displays "No Snapshots found" when there are no snapshots', async () => {
// Mocking the return value with no snapshot data
;(useQuery as jest.Mock).mockReturnValue({
data: { snapshots: [] },
loading: false,
error: null,
})
render(<Snapshots />)

await waitFor(() => {
expect(screen.getByText('No Snapshots found')).toBeInTheDocument()
})
})

test('displays fallback snapshot title if snapshot title is missing', async () => {
// Mocking the return value with a snapshot that has no title
;(useQuery as jest.Mock).mockReturnValue({
data: { snapshots: [{ id: '1', title: '', createdAt: '2021-01-01' }] },
loading: false,
error: null,
})
render(<Snapshots />)

await waitFor(() => {
expect(screen.getByText('Untitled Snapshot')).toBeInTheDocument()
})
})
})
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SnapshotDetailsPage,
UserDetailsPage,
UsersPage,
Snapshots,
} from 'pages'
import { useEffect } from 'react'
import { Routes, Route, useLocation } from 'react-router-dom'
Expand Down Expand Up @@ -47,6 +48,7 @@ function App() {
<Route path="/chapters/:chapterKey" element={<ChapterDetailsPage />}></Route>
<Route path="/community/snapshots/:id" element={<SnapshotDetailsPage />}></Route>
<Route path="/community/users" element={<UsersPage />}></Route>
<Route path="/community/snapshots" element={<Snapshots />}></Route>
<Route path="/community/users/:userKey" element={<UserDetailsPage />}></Route>
<Route path="*" element={<ErrorDisplay {...ERROR_CONFIGS['404']} />} />
</Routes>
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/api/queries/snapshotQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,14 @@ export const GET_SNAPSHOT_DETAILS = gql`
}
}
`

export const GET_COMMUNITY_SNAPSHOTS = gql`
query GetCommunitySnapshots {
snapshots(limit: 24) {
key
title
startAt
endAt
}
}
`
107 changes: 80 additions & 27 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,48 @@ export default function Header() {
{/* Desktop Header Links */}
<div className="hidden flex-1 justify-between rounded-lg pl-6 font-medium md:block">
<div className="flex justify-start pl-6">
{headerLinks.map((link, i) => (
<NavLink
key={i}
to={link.href}
className={cn(
'navlink px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === link.href && 'font-bold text-blue-800 dark:text-white'
)}
aria-current="page"
>
{link.text}
</NavLink>
))}
{headerLinks.map((link) => {
return link.submenu ? (
<div
key={link.text}
className={cn(
'dropdown navlink group px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
link.submenu.map((sub) => sub.href).includes(location.pathname) &&
'font-bold text-blue-800 dark:text-white'
)}
>
{link.text}
<div className="dropdown-menu group-hover:visible group-hover:opacity-100">
{link.submenu.map((sub) => (
<NavLink
key={link.text}
to={sub.href}
className={cn(
'navlink px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === sub.href &&
'font-bold text-blue-800 dark:text-white'
)}
aria-current="page"
>
{sub.text}
</NavLink>
))}
</div>
</div>
) : (
<NavLink
key={link.text}
to={link.href}
className={cn(
'navlink px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === link.href && 'font-bold text-blue-800 dark:text-white'
)}
aria-current="page"
>
{link.text}
</NavLink>
)
})}
</div>
</div>
<div className="flex items-center justify-normal space-x-4">
Expand Down Expand Up @@ -137,20 +166,44 @@ export default function Header() {
</div>
</div>
</NavLink>
{headerLinks.map((link, i) => (
<NavLink
key={i}
to={link.href}
className={cn(
'navlink block px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === link.href &&
'font-bold text-blue-800 dark:text-white'
)}
onClick={toggleMobileMenu}
>
{link.text}
</NavLink>
))}
{headerLinks.map((link) =>
link.submenu ? (
<div key={link.text} className="flex flex-col">
<div className="block px-3 py-2 font-medium text-slate-700 dark:text-slate-300">
{link.text}
</div>
<div className="ml-4">
{link.submenu.map((sub) => (
<NavLink
key={link.text}
to={sub.href}
className={cn(
'navlink block px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === sub.href &&
'font-bold text-blue-800 dark:text-white'
)}
onClick={toggleMobileMenu}
>
{sub.text}
</NavLink>
))}
</div>
</div>
) : (
<NavLink
key={link.text}
to={link.href}
className={cn(
'navlink block px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === link.href &&
'font-bold text-blue-800 dark:text-white'
)}
onClick={toggleMobileMenu}
>
{link.text}
</NavLink>
)
)}
</div>

<div className="flex flex-col gap-y-2">
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/SnapshotCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Button } from '@chakra-ui/react'
import { faChevronRight, faCalendar } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { SnapshotCardProps } from 'types/card'
import { formatDate } from 'utils/dateFormatter'

const SnapshotCard = ({ title, button, startAt, endAt }: SnapshotCardProps) => {
return (
<Button
onClick={button.onclick}
className="group flex h-40 w-full flex-col items-center rounded-lg bg-white p-6 text-left shadow-lg transition-transform duration-500 hover:scale-105 hover:shadow-xl dark:bg-gray-800 dark:shadow-gray-900/30"
>
<div className="text-center">
<h3 className="max-w-[250px] text-balance text-lg font-semibold text-gray-900 group-hover:text-blue-500 dark:text-white sm:text-xl">
<p>{title}</p>
</h3>
</div>

<div className="flex flex-wrap items-center gap-2 text-gray-600 dark:text-gray-300">
<div className="flex items-center">
<FontAwesomeIcon icon={faCalendar} className="mr-1 h-4 w-4" />
<span>
{formatDate(startAt)} - {formatDate(endAt)}
</span>
</div>
</div>

<div className="mt-auto inline-flex items-center text-sm font-medium text-blue-500 dark:text-blue-400">
View Snapshot
<FontAwesomeIcon
icon={faChevronRight}
className="ml-2 h-4 w-4 transform transition-transform group-hover:translate-x-1"
/>
</div>
</Button>
)
}

export default SnapshotCard
16 changes: 16 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ a {
color: inherit;
}

/* Dropdown container */
.dropdown {
@apply relative;
}

.dropdown-menu {
@apply absolute left-0 top-full mt-2 w-48 rounded-lg bg-white p-3 shadow-lg dark:bg-gray-800;
@apply opacity-0 invisible transition-all duration-200 ease-in-out;
@apply flex flex-col space-y-2; /* Stack items vertically */
}

.dropdown:hover .dropdown-menu {
@apply opacity-100 visible;
}


@keyframes fadeIn {
from {
opacity: 0;
Expand Down
Loading