Skip to content

Commit c14abdd

Browse files
committed
Merge branch 'develop'
2 parents 976be7b + 1aba5ac commit c14abdd

File tree

17 files changed

+1537
-8
lines changed

17 files changed

+1537
-8
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@
4343
"clsx": "^2.1.1",
4444
"cmdk": "1.0.0",
4545
"date-fns": "^4.1.0",
46+
"html5-qrcode": "^2.3.8",
4647
"embla-carousel-react": "^8.5.2",
4748
"framer-motion": "^11.18.2",
4849
"input-otp": "^1.4.2",
4950
"lucide-react": "^0.468.0",
5051
"next": "15.0.4",
5152
"next-themes": "^0.4.4",
53+
"qrcode.react": "^4.2.0",
5254
"react": "^19.0.0",
5355
"react-day-picker": "8.10.1",
5456
"react-dom": "^19.0.0",
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"use client";
2+
import { QRCodeSVG } from "qrcode.react";
3+
import { X } from "lucide-react";
4+
import { useState } from "react";
5+
import Block from "@/components/Block";
6+
7+
type Stamp = {
8+
type: string;
9+
isfinished: boolean;
10+
prizeBlockType: string;
11+
};
12+
13+
type Achievement = {
14+
progress: number;
15+
target: number;
16+
name: string;
17+
description: string;
18+
prizeBlockType: string;
19+
};
20+
21+
const stamps: Stamp[] = [
22+
{ type: "1", isfinished: true, prizeBlockType: "1" },
23+
{ type: "2", isfinished: false, prizeBlockType: "2" },
24+
{ type: "3", isfinished: true, prizeBlockType: "3" },
25+
{ type: "4", isfinished: false, prizeBlockType: "4" },
26+
{ type: "5", isfinished: true, prizeBlockType: "5" },
27+
{ type: "6", isfinished: false, prizeBlockType: "6" },
28+
{ type: "7", isfinished: true, prizeBlockType: "7" },
29+
{ type: "8", isfinished: false, prizeBlockType: "8" },
30+
];
31+
32+
const achievements: Achievement[] = [
33+
{
34+
progress: 1,
35+
target: 5,
36+
name: "成就1",
37+
description: "lorem",
38+
prizeBlockType: "1",
39+
},
40+
{
41+
progress: 2,
42+
target: 5,
43+
name: "成就2",
44+
description: "lorem",
45+
prizeBlockType: "2",
46+
},
47+
{
48+
progress: 1,
49+
target: 1,
50+
name: "成就3",
51+
description: "lorem",
52+
prizeBlockType: "3",
53+
},
54+
{
55+
progress: 0,
56+
target: 1,
57+
name: "成就4",
58+
prizeBlockType: "4",
59+
description: "lorem",
60+
},
61+
{
62+
progress: 3,
63+
target: 5,
64+
name: "成就5",
65+
prizeBlockType: "5",
66+
description: "lorem",
67+
},
68+
{
69+
progress: 0,
70+
target: 1,
71+
name: "成就6",
72+
prizeBlockType: "6",
73+
description: "lorem",
74+
},
75+
{
76+
progress: 4,
77+
target: 5,
78+
name: "成就7",
79+
prizeBlockType: "7",
80+
description: "lorem",
81+
},
82+
];
83+
84+
export default function AchievementsPage() {
85+
const [popupType, setPopupType] = useState<"stamp" | "achievement" | null>(
86+
null,
87+
);
88+
const [stamp, setStamp] = useState<Stamp | null>(null);
89+
const [achievement, setAchievement] = useState<Achievement | null>(null);
90+
91+
const handleStampClick = (stamp: Stamp) => {
92+
setStamp(stamp);
93+
setPopupType("stamp");
94+
};
95+
96+
const handleAchievementClick = (achievement: Achievement) => {
97+
setAchievement(achievement);
98+
setPopupType("achievement");
99+
};
100+
101+
return (
102+
<>
103+
<div className="relative px-6 pb-6 pt-16">
104+
<div className="mb-6 grid grid-cols-4 gap-4">
105+
{stamps.map((stamp) => (
106+
<img
107+
key={stamp.type}
108+
src={`https://picsum.photos/id/${stamp.type}/50/50`}
109+
alt={`${stamp.type}_stamp`}
110+
className={`${stamp.isfinished ? "opacity-100" : "opacity-50"}`} // TODO: 替換成icon
111+
onClick={() => handleStampClick(stamp)}
112+
/>
113+
))}
114+
</div>
115+
116+
<div className="flex flex-col gap-8 pr-4">
117+
{achievements.map((achievement, index) => (
118+
<div key={achievement.name} className="flex items-center gap-4">
119+
<img
120+
key={achievement.name}
121+
src={`https://picsum.photos/id/${index}/50/50`}
122+
alt={`${index}_achievement`}
123+
onClick={() => handleAchievementClick(achievement)}
124+
/>
125+
<div className="flex w-full flex-col gap-1">
126+
<div className="flex flex-row justify-between">
127+
<p>{achievement.name}</p>
128+
<p>
129+
{achievement.progress} / {achievement.target}
130+
</p>
131+
</div>
132+
<div className="h-4 w-full rounded-full bg-gray-300">
133+
<div
134+
className="h-full rounded-full bg-green-500"
135+
style={{
136+
width: `${(achievement.progress / achievement.target) * 100}%`,
137+
}}
138+
/>
139+
</div>
140+
</div>
141+
</div>
142+
))}
143+
</div>
144+
{popupType === "stamp" && stamp && (
145+
<StampPopup
146+
stamp={stamp}
147+
closePopup={() => setPopupType(null)}
148+
playerId="123" //TODO: 需要找到時機fetch本地玩家id
149+
/>
150+
)}
151+
{popupType === "achievement" && achievement && (
152+
<AchievementPopup
153+
achievement={achievement}
154+
closePopup={() => setPopupType(null)}
155+
/>
156+
)}
157+
</div>
158+
</>
159+
);
160+
}
161+
162+
const StampPopup = ({
163+
stamp,
164+
closePopup,
165+
playerId,
166+
}: {
167+
stamp: Stamp;
168+
closePopup: () => void;
169+
playerId: string;
170+
}) => {
171+
return (
172+
<>
173+
{stamp && (
174+
<div className="absolute left-1/2 top-1/2 z-50 flex h-[70%] w-[80%] -translate-x-1/2 -translate-y-1/2 transform items-center justify-center rounded-lg border-2 border-[#6558f5] bg-white p-4 shadow-lg">
175+
<X onClick={closePopup} className="absolute right-2 top-2" />
176+
{!stamp.isfinished && (
177+
<div className="flex h-full w-full flex-col items-center gap-4 py-4">
178+
<h3>將QRcode給攤位工作人員即可獲得獎勵方塊!</h3>
179+
<QRCodeSVG width={200} height={200} value={playerId} />
180+
</div>
181+
)}
182+
{stamp.isfinished && (
183+
<div className="flex flex-col gap-4">
184+
<h3>這是你的獎勵方塊</h3>
185+
<Block type={stamp.prizeBlockType} quantity={1} />
186+
</div>
187+
)}
188+
</div>
189+
)}
190+
</>
191+
);
192+
};
193+
194+
const AchievementPopup = ({
195+
achievement,
196+
closePopup,
197+
}: {
198+
achievement: Achievement;
199+
closePopup: () => void;
200+
}) => {
201+
return (
202+
<>
203+
{achievement && (
204+
<div className="absolute left-1/2 top-1/2 z-50 h-[70%] w-[80%] -translate-x-1/2 -translate-y-1/2 transform rounded-lg border-2 border-[#6558f5] bg-white p-8 shadow-lg">
205+
<X onClick={closePopup} className="absolute right-2 top-2" />
206+
{achievement.progress !== achievement.target && (
207+
<div className="flex h-full w-full flex-col items-center gap-4 py-4">
208+
<h3>
209+
{achievement.name} {achievement.progress} / {achievement.target}
210+
</h3>
211+
<p>{achievement.description}</p>
212+
</div>
213+
)}
214+
{achievement.progress === achievement.target && (
215+
<div className="flex flex-col gap-4">
216+
<h3>這是你的獎勵方塊</h3>
217+
<Block type={achievement.prizeBlockType} quantity={1} />
218+
</div>
219+
)}
220+
</div>
221+
)}
222+
</>
223+
);
224+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { LogOut } from "lucide-react";
5+
import { usePathname } from "next/navigation";
6+
export default function Layout({
7+
children,
8+
}: Readonly<{
9+
children: React.ReactNode;
10+
}>) {
11+
const pathname = usePathname();
12+
13+
return (
14+
<div className="flex h-full w-full flex-col">
15+
<section>
16+
<LogOut
17+
onClick={() => console.log("logout")} // TODO: 登入要做的事?
18+
className="absolute right-6 top-6 z-20"
19+
size={32}
20+
/>
21+
</section>
22+
<div className="w-full flex-1 overflow-y-scroll">{children}</div>
23+
<section className="flex w-full gap-[2px]">
24+
<InnerBarItem href="/fragment/link" name="玩家連結" />
25+
<InnerBarItem href="/fragment/share" name="計畫共享" />
26+
<InnerBarItem href="/fragment/achievements" name="成就解鎖" />
27+
</section>
28+
</div>
29+
);
30+
}
31+
32+
function InnerBarItem({
33+
href,
34+
name,
35+
}: Readonly<{
36+
href: string;
37+
name: string;
38+
}>) {
39+
const pathname = usePathname();
40+
41+
const isActive = pathname.includes(href);
42+
43+
return (
44+
<Link
45+
href={href}
46+
className="relative flex w-full flex-1 justify-center bg-[#4b5c6bff] py-2 text-white"
47+
>
48+
{isActive && (
49+
<svg
50+
className="absolute -top-[10px]"
51+
width="20"
52+
height="12"
53+
xmlns="http://www.w3.org/2000/svg"
54+
>
55+
<polygon points="10,0 20,10 0,10 20,12 0,12" fill="#4b5c6bff" />
56+
</svg>
57+
)}
58+
{name}
59+
</Link>
60+
);
61+
}

0 commit comments

Comments
 (0)