diff --git a/app/me/hackathons/submissions/page.tsx b/app/me/hackathons/submissions/page.tsx index c1ff1b6d9..107919de4 100644 --- a/app/me/hackathons/submissions/page.tsx +++ b/app/me/hackathons/submissions/page.tsx @@ -1,7 +1,370 @@ -import { redirect } from 'next/navigation'; +'use client'; -const page = () => { - redirect('/coming-soon'); -}; +import { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useAuthStatus } from '@/hooks/use-auth'; +import BoundlessSheet from '@/components/sheet/boundless-sheet'; +import EmptyState from '@/components/EmptyState'; +import { useRouter } from 'next/navigation'; +import { Trophy } from 'lucide-react'; +import { + Table, + TableBody, + TableHead, + TableHeader, + TableRow as ShadcnTableRow, +} from '@/components/ui/table'; +import { + SortField, + SortDir, + SubmissionRow, + SortIcon, + SubmissionsSheetContent, + TableRow, +} from './submission-components'; -export default page; +export default function SubmissionsPage() { + const router = useRouter(); + const { user, isLoading } = useAuthStatus(); + + const [sortField, setSortField] = useState('submittedAt'); + const [sortDir, setSortDir] = useState('desc'); + const [selectedSubmission, setSelectedSubmission] = + useState(null); + const [sheetOpen, setSheetOpen] = useState(false); + + // Pull submissions data from auth state — no extra API calls + const rawSubmissions: SubmissionRow[] = useMemo(() => { + const profile = (user as any)?.profile; + if (!profile) return []; + + // Primary path: profile.user.hackathonSubmissionsAsParticipant + const fromUser: any[] = + profile?.user?.hackathonSubmissionsAsParticipant || []; + // Secondary alias (some API shapes expose it at profile level) + const fromProfile: any[] = profile?.hackathonSubmissionsAsParticipant || []; + + // Merge & deduplicate by id + const merged = [...fromUser, ...fromProfile]; + const seen = new Set(); + const deduped = merged.filter(s => { + const id = s?.id || s?._id; + if (!id || seen.has(id)) return false; + seen.add(id); + return true; + }); + + return deduped.map((s: any) => ({ + id: s.id || s._id || '', + projectName: s.projectName || s.title || s.name || 'Untitled Submission', + description: s.description, + introduction: s.introduction, + logo: s.logo, + videoUrl: s.videoUrl, + category: s.category, + links: s.links, + status: s.status || 'draft', + rank: s.rank ?? null, + submittedAt: s.submittedAt || s.submissionDate || s.createdAt || '', + votes: s.votes, + comments: s.comments, + hackathon: s.hackathon, + disqualificationReason: s.disqualificationReason, + })); + }, [user]); + + const sorted = useMemo(() => { + return [...rawSubmissions].sort((a, b) => { + let aVal: any; + let bVal: any; + + switch (sortField) { + case 'projectName': + aVal = (a.projectName || '').toLowerCase(); + bVal = (b.projectName || '').toLowerCase(); + break; + case 'hackathon': + aVal = (a.hackathon?.title || a.hackathon?.name || '').toLowerCase(); + bVal = (b.hackathon?.title || b.hackathon?.name || '').toLowerCase(); + break; + case 'status': + aVal = (a.status || '').toLowerCase(); + bVal = (b.status || '').toLowerCase(); + break; + case 'submittedAt': + aVal = a.submittedAt ? new Date(a.submittedAt).getTime() : 0; + bVal = b.submittedAt ? new Date(b.submittedAt).getTime() : 0; + break; + case 'rank': + aVal = a.rank ?? Infinity; + bVal = b.rank ?? Infinity; + break; + default: + return 0; + } + + if (aVal < bVal) return sortDir === 'asc' ? -1 : 1; + if (aVal > bVal) return sortDir === 'asc' ? 1 : -1; + return 0; + }); + }, [rawSubmissions, sortField, sortDir]); + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDir(d => (d === 'asc' ? 'desc' : 'asc')); + } else { + setSortField(field); + setSortDir('asc'); + } + }; + + const handleRowClick = (submission: SubmissionRow) => { + setSelectedSubmission(submission); + setSheetOpen(true); + }; + + const getAriaSort = (field: SortField) => { + if (sortField !== field) return 'none'; + return sortDir === 'asc' ? 'ascending' : 'descending'; + }; + + const thClass = + 'h-12 px-2 text-left align-middle font-medium text-zinc-400 [&:has([role=checkbox])]:pr-0'; + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Page header */} + +

+ My Submissions +

+

+ Track the full lifecycle of every hackathon submission you've + made. +

+
+ + {/* Summary strip */} + {rawSubmissions.length > 0 && ( + + {( + [ + { + label: 'Total', + value: rawSubmissions.length, + color: 'text-white', + }, + { + label: 'Ranked', + value: rawSubmissions.filter(s => { + const st = (s.status || '').toLowerCase(); + return ( + st === 'ranked' || st === 'shortlisted' || st === 'winner' + ); + }).length, + color: 'text-primary', + }, + { + label: 'Under Review', + value: rawSubmissions.filter(s => { + const st = (s.status || '') + .toLowerCase() + .replace(/[\s\-_]+/g, '_'); + return st === 'under_review' || st === 'submitted'; + }).length, + color: 'text-amber-400', + }, + { + label: 'Draft', + value: rawSubmissions.filter( + s => (s.status || '').toLowerCase() === 'draft' + ).length, + color: 'text-zinc-400', + }, + ] as const + ).map(stat => ( +
+ + {stat.value} + + {stat.label} +
+ ))} +
+ )} + + {/* Table */} + + {sorted.length > 0 ? ( + +
+ + + + + + + + + + + + + + + + + {sorted.map((submission, i) => ( + handleRowClick(submission)} + /> + ))} + +
+
+
+ ) : ( + + router.push('/hackathons')} + /> + + )} +
+ + {/* Detail sheet */} + + {selectedSubmission && ( + + )} + +
+ ); +} diff --git a/app/me/hackathons/submissions/submission-components.tsx b/app/me/hackathons/submissions/submission-components.tsx new file mode 100644 index 000000000..2a6704b85 --- /dev/null +++ b/app/me/hackathons/submissions/submission-components.tsx @@ -0,0 +1,519 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { + ChevronUp, + ChevronDown, + ChevronsUpDown, + CalendarDays, + Trophy, + ExternalLink, + MessageCircle, + ThumbsUp, + FileText, + Layers, +} from 'lucide-react'; +import { Separator } from '@/components/ui/separator'; +import { TableCell, TableRow as ShadcnTableRow } from '@/components/ui/table'; +import Image from 'next/image'; +import { format } from 'date-fns'; +import { useState } from 'react'; + +// ─────────────────────────── Url Sanitization ─────────────────────────── + +export function getSafeUrl(urlString?: string): string | undefined { + if (!urlString) return undefined; + try { + const parsed = new URL(urlString); + if (['http:', 'https:', 'mailto:'].includes(parsed.protocol)) { + return parsed.href; + } + return undefined; + } catch { + return undefined; + } +} + +export type SortField = + | 'projectName' + | 'hackathon' + | 'status' + | 'submittedAt' + | 'rank'; +export type SortDir = 'asc' | 'desc'; + +export type SubmissionRow = { + id: string; + projectName: string; + description?: string; + introduction?: string; + logo?: string; + videoUrl?: string; + category?: string; + links?: Array<{ type: string; url: string }>; + status: string; + rank?: number | null; + submittedAt: string; + votes?: number | any[]; + comments?: number | any[]; + hackathon?: { + id?: string; + title?: string; + name?: string; + startDate?: string; + submissionDeadline?: string; + banner?: string; + }; +}; + +// ─────────────────────────── Status badge ─────────────────────────── + +export function getStatusConfig(status: string): { + label: string; + className: string; +} { + const s = (status || '').toLowerCase(); + + if ( + s === 'ranked' || + s === 'shortlisted' || + s === 'winner' || + s === 'completed' + ) { + return { + label: + s === 'shortlisted' ? 'Ranked' : s.charAt(0).toUpperCase() + s.slice(1), + className: 'text-primary bg-primary/10', + }; + } + if (s === 'under_review' || s === 'under review') { + return { + label: 'Under Review', + className: 'text-amber-400 bg-amber-400/10', + }; + } + if (s === 'submitted') { + return { + label: 'Submitted', + className: 'text-blue-400 bg-blue-400/10', + }; + } + if (s === 'disqualified') { + return { + label: 'Disqualified', + className: 'text-red-400 bg-red-400/10', + }; + } + // draft / default + return { + label: + s === 'draft' + ? 'Draft' + : s.charAt(0).toUpperCase() + s.slice(1) || 'Draft', + className: 'text-gray-400 bg-gray-800/20', + }; +} + +export function StatusBadge({ status }: { status: string }) { + const cfg = getStatusConfig(status); + return ( + + {cfg.label} + + ); +} + +// ─────────────────────────── Sort icon ─────────────────────────── + +export function SortIcon({ + field, + sortField, + sortDir, +}: { + field: SortField; + sortField: SortField; + sortDir: SortDir; +}) { + if (sortField !== field) + return ; + return sortDir === 'asc' ? ( + + ) : ( + + ); +} + +export function SubmissionsSheetContent({ + submission, +}: { + submission: SubmissionRow; +}) { + const hackathonName = + submission.hackathon?.title || + submission.hackathon?.name || + 'Unknown Hackathon'; + + const voteCount = + typeof submission.votes === 'number' + ? submission.votes + : Array.isArray(submission.votes) + ? submission.votes.length + : 0; + + const commentCount = + typeof submission.comments === 'number' + ? submission.comments + : Array.isArray(submission.comments) + ? submission.comments.length + : 0; + + const formatDate = (dateString?: string) => { + if (!dateString) return '—'; + try { + return format(new Date(dateString), 'MMM d, yyyy'); + } catch { + return dateString; + } + }; + + const viewUrl = `/projects/${submission.id}?type=submission`; + + return ( +
+ {/* Header */} + + + {/* Banner / Logo */} + {(submission.hackathon?.banner || submission.logo) && ( +
+ {submission.projectName} + {submission.logo && submission.hackathon?.banner && ( +
+
+ Project logo +
+
+ )} +
+ )} + + {/* Metadata strip */} +
+ {submission.rank != null && ( + + + + Rank #{submission.rank} + + + )} + + + Submitted {formatDate(submission.submittedAt)} + + {submission.category && ( + + + {submission.category} + + )} + {voteCount > 0 && ( + + + {voteCount} vote{voteCount !== 1 ? 's' : ''} + + )} + {commentCount > 0 && ( + + + {commentCount} comment{commentCount !== 1 ? 's' : ''} + + )} +
+ + {/* Hackathon dates */} + {(submission.hackathon?.startDate || + submission.hackathon?.submissionDeadline) && ( +
+ {submission.hackathon.startDate && ( +
+

Hackathon Start

+

+ {formatDate(submission.hackathon.startDate)} +

+
+ )} + {submission.hackathon.submissionDeadline && ( +
+

Submission Deadline

+

+ {formatDate(submission.hackathon.submissionDeadline)} +

+
+ )} +
+ )} + + + + {/* Description */} + {submission.description && ( +
+

+ Description +

+

+ {submission.description} +

+
+ )} + + {/* Introduction */} + {submission.introduction && ( +
+

+ Introduction +

+

+ {submission.introduction} +

+
+ )} + + {/* Video link */} + {submission.videoUrl && getSafeUrl(submission.videoUrl) && ( +
+

+ Demo Video +

+ + + Watch Demo + +
+ )} + + {/* Project links */} + {submission.links && submission.links.length > 0 && ( +
+

+ Project Links +

+
+ {submission.links.map((link, i) => { + const safeUrl = getSafeUrl(link.url); + if (!safeUrl) return null; + return ( + + + {link.type || link.url} + + ); + })} +
+
+ )} + + {/* Disqualification reason */} + {(submission.status || '').toLowerCase() === 'disqualified' && + (submission as any).disqualificationReason && ( +
+

+ Disqualification Reason +

+

+ {(submission as any).disqualificationReason} +

+
+ )} +
+ ); +} + +export function TableRow({ + submission, + index, + onClick, +}: { + submission: SubmissionRow; + index: number; + onClick: () => void; +}) { + const hackathonName = + submission.hackathon?.title || submission.hackathon?.name || '—'; + + const viewUrl = `/projects/${submission.id}?type=submission`; + const [isHoverOrFocus, setIsHoverOrFocus] = useState(false); + + const formatDate = (dateString?: string) => { + if (!dateString) return '—'; + try { + return format(new Date(dateString), 'MMM d, yyyy'); + } catch { + return dateString; + } + }; + + const handleLeftClick = (e: React.MouseEvent) => { + // Left click only + if (e.button !== 0) return; + onClick(); + }; + + const handleAuxClick = (e: React.MouseEvent) => { + // Middle click only -> open in new tab + if (e.button === 1) { + e.stopPropagation(); + window.open(viewUrl, '_blank', 'noopener,noreferrer'); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onClick(); + } else if (e.key === ' ') { + e.preventDefault(); // prevent page scrolling + onClick(); + } + }; + + return ( + setIsHoverOrFocus(true)} + onMouseLeave={() => setIsHoverOrFocus(false)} + role='button' + tabIndex={0} + onKeyDown={handleKeyDown} + aria-label={`View details for ${submission.projectName}`} + > + {/* Project */} + + + + + {/* Hackathon */} + + + {hackathonName} + + + + {/* Status */} + + + + + {/* Date */} + + + {formatDate(submission.submittedAt)} + + + + {/* Rank */} + + {submission.rank != null ? ( + + #{submission.rank} + + ) : ( + + )} + + + {/* Chevron */} + + + + + ); +} diff --git a/app/me/layout.tsx b/app/me/layout.tsx index bcb176d32..5309c061e 100644 --- a/app/me/layout.tsx +++ b/app/me/layout.tsx @@ -21,13 +21,30 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { const hackathonsCount = useMemo(() => { if (!profile) return 0; - // eslint-disable-next-line @typescript-eslint/no-explicit-any const joined = (profile as any)?.user?.joinedHackathons || []; - return joined.length; }, [profile]); + const submissionsCount = useMemo(() => { + if (!profile) return 0; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fromUser = + (profile as any)?.user?.hackathonSubmissionsAsParticipant || []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fromProfile = + (profile as any)?.hackathonSubmissionsAsParticipant || []; + // Deduplicate by id before counting + const merged = [...fromUser, ...fromProfile]; + const seen = new Set(); + return merged.filter((s: any) => { + const id = s?.id || s?._id; + if (!id || seen.has(id)) return false; + seen.add(id); + return true; + }).length; + }, [profile]); + if (isLoading) { return (
@@ -47,7 +64,10 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { > diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index bdc0ce57c..63b9e3383 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -28,7 +28,10 @@ import { import Image from 'next/image'; import Link from 'next/link'; -const getNavigationData = (counts?: { participating?: number }) => ({ +const getNavigationData = (counts?: { + participating?: number; + submissions?: number; +}) => ({ main: [ { title: 'Overview', @@ -73,6 +76,10 @@ const getNavigationData = (counts?: { participating?: number }) => ({ title: 'Submissions', url: '/me/hackathons/submissions', icon: IconUsers, + badge: + counts?.submissions && counts.submissions > 0 + ? counts.submissions.toString() + : undefined, }, ], crowdfunding: [ @@ -114,7 +121,7 @@ export function AppSidebar({ ...props }: { user: UserData; - counts?: { participating?: number }; + counts?: { participating?: number; submissions?: number }; } & React.ComponentProps) { const navigationData = React.useMemo( () => getNavigationData(counts), diff --git a/components/profile/PublicEarningsTab.tsx b/components/profile/PublicEarningsTab.tsx index 1df678cfc..fa50f6aec 100644 --- a/components/profile/PublicEarningsTab.tsx +++ b/components/profile/PublicEarningsTab.tsx @@ -197,7 +197,7 @@ const PublicEarningsTab = ({ {(earnings.activities?.length ?? 0) > 0 && (
-

+

Verified Activity

diff --git a/docs/project-detail-redesign.md b/docs/project-detail-redesign.md index 6957bd63c..5e5c01ffb 100644 --- a/docs/project-detail-redesign.md +++ b/docs/project-detail-redesign.md @@ -8,12 +8,13 @@ The objective is to create a simpler, clearer, and more professional experience --- Design Principles -* Clear primary action above the fold -* Strong visual hierarchy -* Reduced visual noise -* Consistent spacing and typography -* Intentional use of the green accent -* Professional and trustworthy product feel + +- Clear primary action above the fold +- Strong visual hierarchy +- Reduced visual noise +- Consistent spacing and typography +- Intentional use of the green accent +- Professional and trustworthy product feel --- @@ -21,65 +22,72 @@ Layout Structure Desktop Left: Sticky summary sidebar -* Project logo -* Title -* Status and short description -* Progress (votes/funding where applicable) -* Primary CTA (Vote or Back) -* Secondary actions (Share) -* Creator info and external links + +- Project logo +- Title +- Status and short description +- Progress (votes/funding where applicable) +- Primary CTA (Vote or Back) +- Secondary actions (Share) +- Creator info and external links Right: Main content area -* Horizontal tab navigation -* Dynamic tab content -* Optimized reading width for long-form content + +- Horizontal tab navigation +- Dynamic tab content +- Optimized reading width for long-form content > The sidebar remains visible while scrolling to maintain action visibility and improve engagement. Mobile -* Sidebar collapses into a condensed header block -* Horizontal scrollable tab navigation -* Single-column content layout -* Primary CTA positioned prominently without overwhelming the interface + +- Sidebar collapses into a condensed header block +- Horizontal scrollable tab navigation +- Single-column content layout +- Primary CTA positioned prominently without overwhelming the interface > No feature loss between desktop and mobile. --- Component Redesign + 1. Loading State -A simplified skeleton layout that mirrors final structure: -* Sidebar placeholder block -* Tab row skeleton -* Content block placeholders -* Reduced visual motion for a clean, professional feel + A simplified skeleton layout that mirrors final structure: + +- Sidebar placeholder block +- Tab row skeleton +- Content block placeholders +- Reduced visual motion for a clean, professional feel > The loading state communicates layout structure without clutter. --- 2. Sidebar (Desktop) / Header Block (Mobile) -Improvements: -* Strong title hierarchy -* Short description positioned clearly under title -* Simplified progress visualization -* One clearly emphasized primary action -* Secondary actions styled with lower visual weight -* Creator avatar and name placed below primary CTA -* External links grouped and visually subtle + Improvements: + +- Strong title hierarchy +- Short description positioned clearly under title +- Simplified progress visualization +- One clearly emphasized primary action +- Secondary actions styled with lower visual weight +- Creator avatar and name placed below primary CTA +- External links grouped and visually subtle > The goal is clarity and action focus. --- 3. Tab Bar -Tabs: Details, Team, Milestones, Voters (optional), Backers (optional), Comments -Improvements: -* Clear selected state -* Subtle hover state -* Consistent spacing and typography -* Scrollable on mobile -* Optional tabs hidden gracefully when not applicable + Tabs: Details, Team, Milestones, Voters (optional), Backers (optional), Comments + Improvements: + +- Clear selected state +- Subtle hover state +- Consistent spacing and typography +- Scrollable on mobile +- Optional tabs hidden gracefully when not applicable > The tab system is visually lightweight but structurally strong. @@ -87,66 +95,78 @@ Improvements: Tab-Level Redesign Details Tab -* Controlled reading width for markdown content -* Clear heading hierarchy -* Improved paragraph spacing -* Links styled consistently -* Optional media block positioned after introduction -* Improved vertical rhythm + +- Controlled reading width for markdown content +- Clear heading hierarchy +- Improved paragraph spacing +- Links styled consistently +- Optional media block positioned after introduction +- Improved vertical rhythm > Focus: readability and professional presentation. Team Tab -* Grid layout on desktop -* Vertical list on mobile -* Avatar, name, and role hierarchy clearly defined -* Clean spacing between members + +- Grid layout on desktop +- Vertical list on mobile +- Avatar, name, and role hierarchy clearly defined +- Clean spacing between members Empty State Example: + > No team members yet. This project is currently solo. Milestones Tab -* Vertical timeline layout -* Status indicators (Upcoming, Active, Completed) -* Clean separation between stages -* Optional filter alignment + +- Vertical timeline layout +- Status indicators (Upcoming, Active, Completed) +- Clean separation between stages +- Optional filter alignment Empty State Example: + > No milestones have been added yet. Voters Tab -* Clean list layout -* Vote indicators clearly distinguished -* Sorting control aligned with header + +- Clean list layout +- Vote indicators clearly distinguished +- Sorting control aligned with header Empty State Example: + > No votes yet. Be the first to support this project. Backers Tab -* Supporter card or structured list layout -* Clear contribution display -* Encouraging but subtle empty state + +- Supporter card or structured list layout +- Clear contribution display +- Encouraging but subtle empty state Empty State Example: + > Be the first to back this project. Comments Tab -* Structured threaded layout -* Controlled indentation depth -* Clear reply visibility -* Sorting dropdown aligned to section header -* Clean comment input field with clear submit action + +- Structured threaded layout +- Controlled indentation depth +- Clear reply visibility +- Sorting dropdown aligned to section header +- Clean comment input field with clear submit action Empty State Example: + > No comments yet. Start the discussion. --- Empty State Strategy Each tab includes a purposeful empty state to avoid dead screens: -* Encourage action -* Maintain tone consistency -* Support engagement goals + +- Encourage action +- Maintain tone consistency +- Support engagement goals --- @@ -154,70 +174,79 @@ Error State Strategy Each tab and interactive component must handle failures gracefully to maintain clarity, trust, and engagement. General Principles -* Display a clear, concise error message in context -* Offer a retry action when appropriate -* Maintain the same layout structure as loading and empty states -* Keep messaging professional and consistent with tone + +- Display a clear, concise error message in context +- Offer a retry action when appropriate +- Maintain the same layout structure as loading and empty states +- Keep messaging professional and consistent with tone Tab-Level Examples Details / Team / Milestones / Voters / Backers / Comments Tabs - * Error message: “Something went wrong while loading this content.” - * Retry CTA: “Try again” button positioned centrally within the tab content area - * Maintain spacing and typography consistency with other states + +- Error message: “Something went wrong while loading this content.” +- Retry CTA: “Try again” button positioned centrally within the tab content area +- Maintain spacing and typography consistency with other states Comment Submission Failure - * Inline error: “Your comment could not be submitted.” - * Preserve typed input so users don’t lose their content - * Include a retry button adjacent to the input field - Vote / Back Action Failure - * Inline or toast notification: “Your vote could not be recorded.” - * Provide immediate retry option - * Ensure visual distinction from primary actions to avoid confusion +- Inline error: “Your comment could not be submitted.” +- Preserve typed input so users don’t lose their content +- Include a retry button adjacent to the input field + +Vote / Back Action Failure + +- Inline or toast notification: “Your vote could not be recorded.” +- Provide immediate retry option +- Ensure visual distinction from primary actions to avoid confusion Deliverables for Implementation -* Error-state mockups for all tabs and key actions -* Retry interaction designs and user flow diagrams -* Copy for all error messages and notifications + +- Error-state mockups for all tabs and key actions +- Retry interaction designs and user flow diagrams +- Copy for all error messages and notifications --- UX Improvements -* Single primary CTA above the fold -* Sticky action area improves conversion -* Stronger reading experience -* Consistent spacing system -* Clear separation of primary vs secondary actions -* Reduced cognitive load + +- Single primary CTA above the fold +- Sticky action area improves conversion +- Stronger reading experience +- Consistent spacing system +- Clear separation of primary vs secondary actions +- Reduced cognitive load --- Visual System Adjustments -* Reduced shadow usage -* Controlled border radius for consistency -* Green accent reserved primarily for key actions and highlights -* Improved typography hierarchy -* Balanced whitespace for clarity + +- Reduced shadow usage +- Controlled border radius for consistency +- Green accent reserved primarily for key actions and highlights +- Improved typography hierarchy +- Balanced whitespace for clarity > The page now feels lighter, more modern, and more trustworthy. --- Responsiveness Strategy -* Sidebar collapses into header on smaller screens -* Horizontal tab scroll -* Content reflows naturally -* No feature disparity between device sizes + +- Sidebar collapses into header on smaller screens +- Horizontal tab scroll +- Content reflows naturally +- No feature disparity between device sizes --- Deliverables -* Desktop full-page mockups (all tabs) -* Mobile full-page mockups (all tabs) -* Component-level breakdown -* Loading, empty, and error state designs -* Retry interaction and UX flow documentation -* UX recommendations + +- Desktop full-page mockups (all tabs) +- Mobile full-page mockups (all tabs) +- Component-level breakdown +- Loading, empty, and error state designs +- Retry interaction and UX flow documentation +- UX recommendations Figma link: https://www.figma.com/design/EMNGAQl1SGObXcsoa24krt/Boundless_Project-Details?node-id=0-1&t=TAP62qLgaqjB5B1K-1