Skip to content

Commit d73e53f

Browse files
authored
Merge pull request #5 from database-playground/pan93412/dbp-56-implement-mobile-navbar
feat: improve RWD
2 parents cf1ab68 + 15314e5 commit d73e53f

File tree

6 files changed

+149
-52
lines changed

6 files changed

+149
-52
lines changed

app/(app)/challenges/_filter/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export default function FilterSection({
1919
return (
2020
<aside
2121
className={`
22-
flex min-h-[50dvh] w-[25%] flex-col gap-8 rounded bg-gray-100 p-6
22+
flex flex-col gap-8 rounded bg-gray-100 p-6
23+
lg:min-h-[50dvh] lg:w-[25%]
2324
`}
2425
>
2526
<SearchFilterSection value={search} onChange={setSearch} />

app/(app)/challenges/content.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ export default function ChallengePageContent() {
6262
};
6363

6464
return (
65-
<div className="flex w-full gap-4">
65+
<div
66+
className={`
67+
flex w-full flex-col-reverse gap-4
68+
lg:flex-row
69+
`}
70+
>
6671
<FilterSection
6772
search={search}
6873
setSearch={setSearch}
Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,37 @@
11
"use client";
22

33
import { graphql } from "@/gql";
4-
import { useSuspenseQuery } from "@apollo/client/react";
4+
import { useIsMobile } from "@/hooks/use-mobile";
5+
import { skipToken, useSuspenseQuery } from "@apollo/client/react";
56

67
const COMPLETED_QUESTIONS = graphql(`
7-
query CompletedQuestions {
8-
me {
9-
submissionStatistics {
10-
totalQuestions
11-
solvedQuestions
12-
}
13-
}
8+
query CompletedQuestions {
9+
me {
10+
submissionStatistics {
11+
totalQuestions
12+
solvedQuestions
13+
}
1414
}
15+
}
1516
`);
1617

1718
export default function CompletedQuestionsPercentage() {
18-
const { data } = useSuspenseQuery(COMPLETED_QUESTIONS);
19+
const isMobile = useIsMobile();
20+
21+
const { data } = useSuspenseQuery(
22+
COMPLETED_QUESTIONS,
23+
isMobile ? skipToken : undefined,
24+
);
25+
26+
if (!data) return null;
27+
1928
const totalQuestions = data.me.submissionStatistics.totalQuestions;
2029
const solvedQuestions = data.me.submissionStatistics.solvedQuestions;
2130
const completedPercentage = (solvedQuestions / totalQuestions) * 100;
2231

23-
return <>{solvedQuestions}/{totalQuestions} ({completedPercentage.toFixed(2)}%)</>;
32+
return (
33+
<>
34+
{solvedQuestions}/{totalQuestions} ({completedPercentage.toFixed(2)}%)
35+
</>
36+
);
2437
}

app/(app)/statistics/board/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default function Board() {
55
return (
66
<section
77
className={`
8-
relative mb-6 flex h-42 w-full items-end justify-between rounded
8+
relative mb-6 flex min-h-42 w-full items-end justify-between rounded
99
bg-primary/20 px-6 py-4
1010
`}
1111
>
@@ -21,7 +21,12 @@ export default function Board() {
2121
</div>
2222

2323
<Suspense>
24-
<div className="flex flex-col items-end leading-none">
24+
<div
25+
className={`
26+
hidden flex-col items-end leading-none
27+
lg:flex
28+
`}
29+
>
2530
<p className="text-sm text-muted-foreground">完成題數</p>
2631
<p className="text-xl font-bold">
2732
<CompletedQuestionsPercentage />

components/app-navbar.tsx

Lines changed: 102 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { Logo } from "@/components/logo";
33
import { Button } from "@/components/ui/button";
44
import useUser from "@/hooks/use-user";
55
import { cn } from "@/lib/utils";
6-
import { BarChart3, BookOpen, ChevronDown, MessageSquare, Swords } from "lucide-react";
6+
import { BarChart3, BookOpen, ChevronDown, Menu, MessageSquare, Swords, X } from "lucide-react";
77
import Link from "next/link";
8-
import { Suspense } from "react";
8+
import { Suspense, useState } from "react";
9+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";
910
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
1011

1112
interface NavItemProps {
@@ -19,7 +20,11 @@ function NavItem({ icon, label, active = false }: NavItemProps) {
1920
<Button
2021
variant="ghost"
2122
className={cn(
22-
"flex h-auto items-center gap-3 rounded px-3 py-2 text-sm",
23+
`
24+
flex h-auto w-full items-center justify-start gap-3 rounded px-3 py-2
25+
text-sm
26+
md:w-auto md:justify-center
27+
`,
2328
active && "bg-primary text-primary-foreground",
2429
)}
2530
>
@@ -56,45 +61,107 @@ function UserMenu() {
5661

5762
export default function AppNavbar({ path }: { path: string }) {
5863
const navItemLabel = getActiveNavItemLabel(path);
64+
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
5965

6066
return (
61-
<nav className="border-b border-stone-200 bg-stone-50">
62-
<div className="flex items-center justify-between px-6 py-0">
63-
{/* Left Section */}
64-
<div className="flex items-center gap-4">
65-
{/* Logo and Title */}
66-
<Link href="/">
67-
<div className="flex items-center gap-3 px-3 py-4">
68-
<Logo className="h-4 w-4" />
69-
<span className="whitespace-nowrap text-stone-900">
70-
資料庫練功房
71-
</span>
67+
<Collapsible open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
68+
<nav className="border-b border-stone-200 bg-stone-50">
69+
<div className="flex items-center justify-between px-6 py-0">
70+
{/* Left Section */}
71+
<div className="flex items-center gap-4">
72+
{/* Logo and Title */}
73+
<Link href="/">
74+
<div className="flex items-center gap-3 px-3 py-4">
75+
<Logo className="h-4 w-4" />
76+
<span className="whitespace-nowrap text-stone-900">
77+
資料庫練功房
78+
</span>
79+
</div>
80+
</Link>
81+
82+
{/* Desktop Navigation Items - Hidden on mobile */}
83+
<div
84+
className={`
85+
hidden items-center gap-1
86+
md:flex
87+
`}
88+
>
89+
{navItems.map((item) => (
90+
<Link href={item.pathPrefix} key={item.label}>
91+
<NavItem
92+
key={item.label}
93+
icon={item.icon}
94+
label={item.label}
95+
active={navItemLabel === item.label}
96+
/>
97+
</Link>
98+
))}
7299
</div>
73-
</Link>
100+
</div>
74101

75-
{/* Navigation Items */}
76-
<div className="flex items-center gap-1">
77-
{navItems.map((item) => (
78-
<Link href={item.pathPrefix} key={item.label}>
79-
<NavItem
80-
key={item.label}
81-
icon={item.icon}
82-
label={item.label}
83-
active={navItemLabel === item.label}
84-
/>
85-
</Link>
86-
))}
102+
{/* Right Section */}
103+
<div className="flex items-center gap-2">
104+
{/* Desktop User Menu - Hidden on mobile */}
105+
<div
106+
className={`
107+
hidden
108+
md:block
109+
`}
110+
>
111+
<Suspense>
112+
<UserMenu />
113+
</Suspense>
114+
</div>
115+
116+
{/* Mobile Menu Button - Only visible on mobile */}
117+
<CollapsibleTrigger asChild className="md:hidden">
118+
<Button
119+
variant="ghost"
120+
size="icon"
121+
className="h-10 w-10"
122+
aria-label="Toggle mobile menu"
123+
>
124+
{isMobileMenuOpen ? <X className="h-5 w-5" /> : (
125+
<Menu
126+
className={`h-5 w-5`}
127+
/>
128+
)}
129+
</Button>
130+
</CollapsibleTrigger>
87131
</div>
88132
</div>
89133

90-
{/* Right Section - User Menu */}
91-
<div className="flex items-center">
92-
<Suspense>
93-
<UserMenu />
94-
</Suspense>
95-
</div>
96-
</div>
97-
</nav>
134+
{/* Mobile Navigation Menu */}
135+
<CollapsibleContent className="md:hidden">
136+
<div className="border-t border-stone-200 bg-stone-50">
137+
<div className="flex flex-col space-y-1 px-6 py-4">
138+
{/* Mobile Navigation Items */}
139+
{navItems.map((item) => (
140+
<Link
141+
href={item.pathPrefix}
142+
key={item.label}
143+
onClick={() => setIsMobileMenuOpen(false)}
144+
>
145+
<NavItem
146+
key={item.label}
147+
icon={item.icon}
148+
label={item.label}
149+
active={navItemLabel === item.label}
150+
/>
151+
</Link>
152+
))}
153+
154+
{/* Mobile User Menu */}
155+
<div className="mt-4 border-t border-stone-200 pt-4">
156+
<Suspense>
157+
<UserMenu />
158+
</Suspense>
159+
</div>
160+
</div>
161+
</div>
162+
</CollapsibleContent>
163+
</nav>
164+
</Collapsible>
98165
);
99166
}
100167

gql/gql.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,21 @@ type Documents = {
1818
"\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n": typeof types.QuestionCardFragmentDoc,
1919
"\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\n }\n": typeof types.QuestionSolvedStatusFragmentDoc,
2020
"\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ListQuestionsDocument,
21-
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": typeof types.CompletedQuestionsDocument,
21+
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": typeof types.CompletedQuestionsDocument,
2222
"\n query Points {\n me {\n totalPoints\n\n points(first: 5) {\n edges {\n node {\n id\n ...PointFragment\n }\n }\n }\n }\n }\n": typeof types.PointsDocument,
2323
"\n fragment PointFragment on Point {\n description\n points\n }\n": typeof types.PointFragmentFragmentDoc,
24+
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": typeof types.CompletedQuestionsDocument,
2425
"\n query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": typeof types.BasicUserInfoDocument,
2526
};
2627
const documents: Documents = {
2728
"\n query ChallengeStatisticsQuery {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": types.ChallengeStatisticsQueryDocument,
2829
"\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n": types.QuestionCardFragmentDoc,
2930
"\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\n }\n": types.QuestionSolvedStatusFragmentDoc,
3031
"\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ListQuestionsDocument,
31-
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": types.CompletedQuestionsDocument,
32+
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": types.CompletedQuestionsDocument,
3233
"\n query Points {\n me {\n totalPoints\n\n points(first: 5) {\n edges {\n node {\n id\n ...PointFragment\n }\n }\n }\n }\n }\n": types.PointsDocument,
3334
"\n fragment PointFragment on Point {\n description\n points\n }\n": types.PointFragmentFragmentDoc,
35+
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": types.CompletedQuestionsDocument,
3436
"\n query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": types.BasicUserInfoDocument,
3537
};
3638

@@ -67,7 +69,7 @@ export function graphql(source: "\n query ListQuestions($where: QuestionWhereIn
6769
/**
6870
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
6971
*/
70-
export function graphql(source: "\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n"): (typeof documents)["\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n"];
72+
export function graphql(source: "\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n"): (typeof documents)["\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n"];
7173
/**
7274
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
7375
*/
@@ -76,6 +78,10 @@ export function graphql(source: "\n query Points {\n me {\n
7678
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
7779
*/
7880
export function graphql(source: "\n fragment PointFragment on Point {\n description\n points\n }\n"): (typeof documents)["\n fragment PointFragment on Point {\n description\n points\n }\n"];
81+
/**
82+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
83+
*/
84+
export function graphql(source: "\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n"): (typeof documents)["\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n"];
7985
/**
8086
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
8187
*/

0 commit comments

Comments
 (0)