-
+
-
+ Get started by editing{" "}
+
+ src/app/page.tsx +
+ . +
+ - + Save and see your changes instantly. + +
dAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJw b z_^v8bbg` SAn{I*4bH$u(RZ6*x UhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=p C^ S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk( $?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU ^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvh CL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c 70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397* _cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111a H}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*I cmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU &68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-= A= yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v #ix45EVrcEhr>!NMhprl $InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~ &^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7< 4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}sc Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+ 9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2 `1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M =hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S( O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..a932894 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,103 @@ +import Image from "next/image"; + +export default function Home() { + return ( + ++ ); +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..c133409 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From fa6c1b393cc8a51abf7cbd2e039985a5688626a3 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 14:52:58 +0800 Subject: [PATCH 04/97] Add ci workflow for checking formatting and syntax errors --- .github/workflows/lint.yml | 22 ++++++++++++++++++++++ frontend/package.json | 4 +++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..1c19357 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint CI +on: + pull_request: + branches: + - master + +jobs: + check-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - run: npm ci + + - run: npm run lint + + - run: npm run format diff --git a/frontend/package.json b/frontend/package.json index d9afd3c..b6e65e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,9 @@ "dev": "next dev --turbopack", "build": "next build --turbopack", "start": "next start", - "lint": "eslint" + "lint": "next lint", + "format": "prettier --check .", + "format:fix": "prettier --write ." }, "dependencies": { "react": "19.1.0", From 22d145f495d20c7ca503c5db740f25531f673160 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 14:56:34 +0800 Subject: [PATCH 05/97] Fix minor bug --- .github/workflows/lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1c19357..9c39383 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,6 +14,7 @@ jobs: with: node-version: '20' cache: 'npm' + cache-dependency-path: frontend/package-lock.json - run: npm ci From 79c9a4a276ecb903a6f5636f6b6ad514c7a15311 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 15:01:03 +0800 Subject: [PATCH 06/97] Set frontend folder as the root source for Turbopack --- frontend/next.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..85b3211 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,6 +2,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + turbopack: { + root: __dirname, + }, }; export default nextConfig; From aaa68cd09879dc4d37c1dddf780bb84fafdb2943 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 15:03:14 +0800 Subject: [PATCH 07/97] Fix bug where CI is not running on frontend folder --- .github/workflows/lint.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9c39383..4b090d0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,6 +7,11 @@ on: jobs: check-lint: runs-on: ubuntu-latest + + defaults: + run: + working-directory: frontend + steps: - uses: actions/checkout@v4 From 105cebb92ba9f776c26fc9430ac394de108eb621 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 15:04:55 +0800 Subject: [PATCH 08/97] Update dependencies to include prettier --- frontend/package-lock.json | 17 +++++++++++++++++ frontend/package.json | 13 +++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9379c81..979001c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.5.2", + "prettier": "^3.6.2", "tailwindcss": "^4", "typescript": "^5" } @@ -5008,6 +5009,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index b6e65e3..1cf545a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,19 +11,20 @@ "format:fix": "prettier --write ." }, "dependencies": { + "next": "15.5.2", "react": "19.1.0", - "react-dom": "19.1.0", - "next": "15.5.2" + "react-dom": "19.1.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.5.2", - "@eslint/eslintrc": "^3" + "prettier": "^3.6.2", + "tailwindcss": "^4", + "typescript": "^5" } } From 56496058d6997b39f342de5a5c0da923ed56a92d Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 15:10:07 +0800 Subject: [PATCH 09/97] Remove the default Next page --- frontend/src/app/page.tsx | 104 ++------------------------------------ 1 file changed, 5 insertions(+), 99 deletions(-) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index a932894..22b4c9b 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,103 +1,9 @@ -import Image from "next/image"; - export default function Home() { return ( -+ + ++ +
+ + +- + Get started by editing{" "} +
++ src/app/page.tsx +
+ . +- + Save and see your changes instantly. +
+-+- - -- -
- - -- - Get started by editing{" "} -
-- src/app/page.tsx -
- . -- - Save and see your changes instantly. -
-+ ); } From 0364db7670d77df780303f74c421e14d9b19d066 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:26:26 +0800 Subject: [PATCH 10/97] Add shadcn library --- frontend/components.json | 22 ++ frontend/package-lock.json | 190 +++++++++++++++++- frontend/package.json | 12 +- .../src/app/components/auth/LoginForm.tsx | 49 +++++ frontend/src/app/globals.css | 122 ++++++++++- frontend/src/components/ui/button.tsx | 59 ++++++ frontend/src/components/ui/card.tsx | 92 +++++++++ frontend/src/components/ui/form.tsx | 167 +++++++++++++++ frontend/src/components/ui/input.tsx | 21 ++ frontend/src/components/ui/label.tsx | 24 +++ frontend/src/lib/utils.ts | 6 + 11 files changed, 750 insertions(+), 14 deletions(-) create mode 100644 frontend/components.json create mode 100644 frontend/src/app/components/auth/LoginForm.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/form.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/lib/utils.ts diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..761072a --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 979001c..b8bb1f0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,9 +8,18 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.2.1", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.542.0", "next": "15.5.2", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "react-hook-form": "^7.62.0", + "tailwind-merge": "^3.3.1", + "zod": "^4.1.5" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -22,6 +31,7 @@ "eslint-config-next": "15.5.2", "prettier": "^3.6.2", "tailwindcss": "^4", + "tw-animate-css": "^1.3.8", "typescript": "^5" } }, @@ -212,6 +222,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz", + "integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -950,6 +972,85 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -964,6 +1065,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1295,7 +1402,7 @@ "version": "19.1.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1305,7 +1412,7 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -2292,12 +2399,33 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2369,7 +2497,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4476,6 +4604,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "0.542.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz", + "integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.18", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", @@ -5089,6 +5226,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5706,6 +5859,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", @@ -5838,6 +6001,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", + "integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6151,6 +6324,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 1cf545a..9148358 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,18 @@ "format:fix": "prettier --write ." }, "dependencies": { + "@hookform/resolvers": "^5.2.1", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.542.0", "next": "15.5.2", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "react-hook-form": "^7.62.0", + "tailwind-merge": "^3.3.1", + "zod": "^4.1.5" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -25,6 +34,7 @@ "eslint-config-next": "15.5.2", "prettier": "^3.6.2", "tailwindcss": "^4", + "tw-animate-css": "^1.3.8", "typescript": "^5" } } diff --git a/frontend/src/app/components/auth/LoginForm.tsx b/frontend/src/app/components/auth/LoginForm.tsx new file mode 100644 index 0000000..ea2d9f2 --- /dev/null +++ b/frontend/src/app/components/auth/LoginForm.tsx @@ -0,0 +1,49 @@ +"use cilent"; + +import { Button } from "@/components/ui/button"; +import { Card, CardTitle, CardHeader, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function LoginForm() { + + + return ( ++ hi +
++ + ) +} \ No newline at end of file diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index a2dc41e..20db080 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,26 +1,130 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { +@media (prefers-color-scheme: light) { :root { --background: #0a0a0a; --foreground: #ededed; } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +:root { + --radius: 0.625rem; + --background: #0a0a0a; + --foreground: #ededed; + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.21 0.006 285.885); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.705 0.015 286.067); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.21 0.006 285.885); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.705 0.015 286.067); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.92 0.004 286.32); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.552 0.016 285.938); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.552 0.016 285.938); } + +@layer base { + * { + @apply border-border outline-ring/50; + } + + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps+ + ++ Welcome to Peerprep + ++ + + +& { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx new file mode 100644 index 0000000..524b986 --- /dev/null +++ b/frontend/src/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath , +> = { + name: TName +} + +const FormFieldContext = React.createContext ( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath , +>({ + ...props +}: ControllerProps ) => { + return ( + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within+ ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext ( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + + + + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( + + ) +} + +function FormControl({ ...props }: React.ComponentProps ) { + const { error, formItemId, formDescriptionId, formMessageId } = useFormField() + + return ( + + ) +} + +function FormDescription({ className, ...props }: React.ComponentProps<"p">) { + const { formDescriptionId } = useFormField() + + return ( + + ) +} + +function FormMessage({ className, ...props }: React.ComponentProps<"p">) { + const { error, formMessageId } = useFormField() + const body = error ? String(error?.message ?? "") : props.children + + if (!body) { + return null + } + + return ( + + {body} +
+ ) +} + +export { + useFormField, + Form, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + FormField, +} diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..03295ca --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..fb5fbc3 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} From 6925ddf79f8da8dae8c2749e4b821e75a704fa06 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:26:43 +0800 Subject: [PATCH 11/97] Add skeleton pages for both login and signup page --- frontend/src/app/auth/login/page.tsx | 10 ++++++++++ frontend/src/app/auth/signup/page.tsx | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 frontend/src/app/auth/login/page.tsx create mode 100644 frontend/src/app/auth/signup/page.tsx diff --git a/frontend/src/app/auth/login/page.tsx b/frontend/src/app/auth/login/page.tsx new file mode 100644 index 0000000..4b3ba48 --- /dev/null +++ b/frontend/src/app/auth/login/page.tsx @@ -0,0 +1,10 @@ +import LoginForm from "@/app/components/auth/LoginForm"; + + +export default function LoginPage() { + return ( + ++ ) +} \ No newline at end of file diff --git a/frontend/src/app/auth/signup/page.tsx b/frontend/src/app/auth/signup/page.tsx new file mode 100644 index 0000000..43d2d93 --- /dev/null +++ b/frontend/src/app/auth/signup/page.tsx @@ -0,0 +1,7 @@ +export default function SignUpPage() { + return ( ++ + SignUp Page ++ ) +} \ No newline at end of file From 12170736e4f68dcfe4d75c9372d147191c88a9ea Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:31:41 +0800 Subject: [PATCH 12/97] Add signup portion in Login Page --- frontend/src/app/components/auth/LoginForm.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/auth/LoginForm.tsx b/frontend/src/app/components/auth/LoginForm.tsx index ea2d9f2..26c3409 100644 --- a/frontend/src/app/components/auth/LoginForm.tsx +++ b/frontend/src/app/components/auth/LoginForm.tsx @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardTitle, CardHeader, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import Link from "next/link"; export default function LoginForm() { @@ -34,12 +35,21 @@ export default function LoginForm() { --From be1f47592926e644015db280577b8f0b3a2c4824 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:40:32 +0800 Subject: [PATCH 13/97] Add UI for signup form --- frontend/src/app/auth/signup/page.tsx | 6 +- .../src/app/components/auth/SignUpForm.tsx | 58 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/components/auth/SignUpForm.tsx diff --git a/frontend/src/app/auth/signup/page.tsx b/frontend/src/app/auth/signup/page.tsx index 43d2d93..4b08986 100644 --- a/frontend/src/app/auth/signup/page.tsx +++ b/frontend/src/app/auth/signup/page.tsx @@ -1,7 +1,9 @@ +import SignupForm from "@/app/components/auth/SignUpForm"; + export default function SignUpPage() { return ( -- SignUp Page ++) } \ No newline at end of file diff --git a/frontend/src/app/components/auth/SignUpForm.tsx b/frontend/src/app/components/auth/SignUpForm.tsx new file mode 100644 index 0000000..65c48e7 --- /dev/null +++ b/frontend/src/app/components/auth/SignUpForm.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Card, CardTitle, CardHeader, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function SignupForm() { + + function NavigateToLogin() { + window.location.href = "/auth/login"; + }; + + return ( ++ + ) +} \ No newline at end of file From 3cc576a31be6b66ded3a7ca8d5f0a661a225ea15 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:40:52 +0800 Subject: [PATCH 14/97] Add placeholder logic to navigate to home page --- frontend/src/app/components/auth/LoginForm.tsx | 10 +++++++--- frontend/src/app/home/page.tsx | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 frontend/src/app/home/page.tsx diff --git a/frontend/src/app/components/auth/LoginForm.tsx b/frontend/src/app/components/auth/LoginForm.tsx index 26c3409..fe453ac 100644 --- a/frontend/src/app/components/auth/LoginForm.tsx +++ b/frontend/src/app/components/auth/LoginForm.tsx @@ -1,4 +1,4 @@ -"use cilent"; +"use client"; import { Button } from "@/components/ui/button"; import { Card, CardTitle, CardHeader, CardContent } from "@/components/ui/card"; @@ -9,6 +9,10 @@ import Link from "next/link"; export default function LoginForm() { + function handleLogin() { + window.location.href = "/home"; + }; + return (+ + ++ Register an account + ++ + + +@@ -36,14 +40,14 @@ export default function LoginForm() { -+ handleLogin()} className="w-full"> Login - Don't have an account? + Do not have an account?Signup diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx new file mode 100644 index 0000000..e5e2275 --- /dev/null +++ b/frontend/src/app/home/page.tsx @@ -0,0 +1,7 @@ +export default function HomePage() { + return ( ++ This is a home page ++ ) +} \ No newline at end of file From 974c1d3404c58c2abd813b2378a26636eb7442ee Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 18:07:11 +0800 Subject: [PATCH 15/97] Add components for Home Page --- .../src/app/components/home/StatisticPage.tsx | 9 ++++++ .../src/app/components/home/WelcomePage.tsx | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 frontend/src/app/components/home/StatisticPage.tsx create mode 100644 frontend/src/app/components/home/WelcomePage.tsx diff --git a/frontend/src/app/components/home/StatisticPage.tsx b/frontend/src/app/components/home/StatisticPage.tsx new file mode 100644 index 0000000..9391edc --- /dev/null +++ b/frontend/src/app/components/home/StatisticPage.tsx @@ -0,0 +1,9 @@ +"use client"; + +export default function StatisticPage() { + return ( ++ Statistic Page ++ ) +} \ No newline at end of file diff --git a/frontend/src/app/components/home/WelcomePage.tsx b/frontend/src/app/components/home/WelcomePage.tsx new file mode 100644 index 0000000..d0b7f52 --- /dev/null +++ b/frontend/src/app/components/home/WelcomePage.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { Card, CardTitle, CardHeader, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + + +export default function WelcomePage() { + + return ( ++ + + ) +} \ No newline at end of file From c3c11693efd750f3007f17b0914c3878551d382f Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 18:07:21 +0800 Subject: [PATCH 16/97] Add home page --- frontend/src/app/home/page.tsx | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx index e5e2275..214e76b 100644 --- a/frontend/src/app/home/page.tsx +++ b/frontend/src/app/home/page.tsx @@ -1,7 +1,29 @@ +import StatisticPage from "../components/home/StatisticPage"; +import WelcomePage from "../components/home/WelcomePage"; + + export default function HomePage() { + return ( -+ + ++ Hello + ++ Derrick Wong +
++ Ready to start coding? + ++ + ++ Start + +- This is a home page +) From 14bc4764b6c79b183934a13c611ae449134f8f97 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 19:18:25 +0800 Subject: [PATCH 18/97] Add statistics page containing 4 groups of stats --- .../src/app/components/home/StatisticPage.tsx | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/home/StatisticPage.tsx b/frontend/src/app/components/home/StatisticPage.tsx index 9391edc..c1efeec 100644 --- a/frontend/src/app/components/home/StatisticPage.tsx +++ b/frontend/src/app/components/home/StatisticPage.tsx @@ -1,9 +1,42 @@ "use client"; +import { Card, CardHeader, CardTitle } from "@/components/ui/card"; export default function StatisticPage() { return ( -+ +) } \ No newline at end of file From b395c77efc59bc89c63377bd8ae89448e071f7b2 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 19:18:05 +0800 Subject: [PATCH 17/97] Update contents of components for Home Page --- .../src/app/components/home/HistoryPage.tsx | 20 +++++++++++++++++++ .../app/components/home/QuickActionsPage.tsx | 15 ++++++++++++++ frontend/src/app/home/page.tsx | 7 +++++++ 3 files changed, 42 insertions(+) create mode 100644 frontend/src/app/components/home/HistoryPage.tsx create mode 100644 frontend/src/app/components/home/QuickActionsPage.tsx diff --git a/frontend/src/app/components/home/HistoryPage.tsx b/frontend/src/app/components/home/HistoryPage.tsx new file mode 100644 index 0000000..905e0ba --- /dev/null +++ b/frontend/src/app/components/home/HistoryPage.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; + +export default function HistoryPage() { + return ( +++ ++ hamburger +++ PeerPrep +++ Avatar +++ + + + + + ) +} \ No newline at end of file diff --git a/frontend/src/app/components/home/QuickActionsPage.tsx b/frontend/src/app/components/home/QuickActionsPage.tsx new file mode 100644 index 0000000..ec37591 --- /dev/null +++ b/frontend/src/app/components/home/QuickActionsPage.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { Card, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function QuickActionsPage() { + return ( ++ + ++ Recent Sessions + ++ {/* TODO Add scrollable area of sessions */} + + ++ + ) +} \ No newline at end of file diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx index 214e76b..8800ab1 100644 --- a/frontend/src/app/home/page.tsx +++ b/frontend/src/app/home/page.tsx @@ -1,3 +1,5 @@ +import HistoryPage from "../components/home/HistoryPage"; +import QuickActionsPage from "../components/home/QuickActionsPage"; import StatisticPage from "../components/home/StatisticPage"; import WelcomePage from "../components/home/WelcomePage"; @@ -23,6 +25,11 @@ export default function HomePage() {+ ++ Quick Actions + ++ +++ + - Statistic Page ++ +) } \ No newline at end of file From a43737fa46e515657419da846dd4f9e27c103382 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Sun, 7 Sep 2025 19:37:33 +0800 Subject: [PATCH 19/97] Fix styling using prettier to pass CI workflow --- frontend/src/app/auth/login/page.tsx | 13 ++- frontend/src/app/auth/signup/page.tsx | 12 +-- .../src/app/components/auth/LoginForm.tsx | 99 +++++++++---------- .../src/app/components/auth/SignUpForm.tsx | 87 +++++++--------- .../src/app/components/home/HistoryPage.tsx | 25 ++--- .../app/components/home/QuickActionsPage.tsx | 18 ++-- .../src/app/components/home/StatisticPage.tsx | 60 +++++------ .../src/app/components/home/WelcomePage.tsx | 42 ++++---- frontend/src/app/globals.css | 2 +- frontend/src/app/home/page.tsx | 49 ++++----- frontend/src/app/page.tsx | 4 +- frontend/src/components/ui/button.tsx | 20 ++-- frontend/src/components/ui/card.tsx | 26 ++--- frontend/src/components/ui/form.tsx | 81 +++++++-------- frontend/src/components/ui/input.tsx | 10 +- frontend/src/components/ui/label.tsx | 14 +-- frontend/src/lib/utils.ts | 6 +- 17 files changed, 254 insertions(+), 314 deletions(-) diff --git a/frontend/src/app/auth/login/page.tsx b/frontend/src/app/auth/login/page.tsx index 4b3ba48..861db1d 100644 --- a/frontend/src/app/auth/login/page.tsx +++ b/frontend/src/app/auth/login/page.tsx @@ -1,10 +1,9 @@ import LoginForm from "@/app/components/auth/LoginForm"; - export default function LoginPage() { - return ( -+ + ++ ++ Total time spent coding + ++ + ++ ++ Total Problems Solved + ++ + ++ ++ Sessions Completed + ++ ++ ++ Favourite Topic + +-- ) -} \ No newline at end of file + return ( +- ++ ); +} diff --git a/frontend/src/app/auth/signup/page.tsx b/frontend/src/app/auth/signup/page.tsx index 4b08986..6d8ec26 100644 --- a/frontend/src/app/auth/signup/page.tsx +++ b/frontend/src/app/auth/signup/page.tsx @@ -1,9 +1,9 @@ import SignupForm from "@/app/components/auth/SignUpForm"; export default function SignUpPage() { - return ( -+ -- ) -} \ No newline at end of file + return ( +- ++ ); +} diff --git a/frontend/src/app/components/auth/LoginForm.tsx b/frontend/src/app/components/auth/LoginForm.tsx index fe453ac..5c09c0c 100644 --- a/frontend/src/app/components/auth/LoginForm.tsx +++ b/frontend/src/app/components/auth/LoginForm.tsx @@ -7,57 +7,48 @@ import { Label } from "@/components/ui/label"; import Link from "next/link"; export default function LoginForm() { - - - function handleLogin() { - window.location.href = "/home"; - }; - - return ( -+ - - ) -} \ No newline at end of file + function handleLogin() { + window.location.href = "/home"; + } + + return ( +- - -- Welcome to Peerprep - -- - - -+ + ); +} diff --git a/frontend/src/app/components/auth/SignUpForm.tsx b/frontend/src/app/components/auth/SignUpForm.tsx index 65c48e7..32a26e6 100644 --- a/frontend/src/app/components/auth/SignUpForm.tsx +++ b/frontend/src/app/components/auth/SignUpForm.tsx @@ -6,53 +6,40 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; export default function SignupForm() { - - function NavigateToLogin() { - window.location.href = "/auth/login"; - }; - - return ( -+ + ++ Welcome to Peerprep + ++ + +- - ) -} \ No newline at end of file + function NavigateToLogin() { + window.location.href = "/auth/login"; + } + + return ( +- - -- Register an account - -- - - -+ + ); +} diff --git a/frontend/src/app/components/home/HistoryPage.tsx b/frontend/src/app/components/home/HistoryPage.tsx index 905e0ba..17108ac 100644 --- a/frontend/src/app/components/home/HistoryPage.tsx +++ b/frontend/src/app/components/home/HistoryPage.tsx @@ -3,18 +3,13 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; export default function HistoryPage() { - return ( -+ + ++ Register an account + ++ + +- - ) -} \ No newline at end of file + return ( +- - -- Recent Sessions - -- {/* TODO Add scrollable area of sessions */} - - -+ + ); +} diff --git a/frontend/src/app/components/home/QuickActionsPage.tsx b/frontend/src/app/components/home/QuickActionsPage.tsx index ec37591..41d06fa 100644 --- a/frontend/src/app/components/home/QuickActionsPage.tsx +++ b/frontend/src/app/components/home/QuickActionsPage.tsx @@ -3,13 +3,11 @@ import { Card, CardHeader, CardTitle } from "@/components/ui/card"; export default function QuickActionsPage() { - return ( -+ + +Recent Sessions +{/* TODO Add scrollable area of sessions */} +- - ) -} \ No newline at end of file + return ( +- -- Quick Actions - -+ + ); +} diff --git a/frontend/src/app/components/home/StatisticPage.tsx b/frontend/src/app/components/home/StatisticPage.tsx index c1efeec..f4fd74a 100644 --- a/frontend/src/app/components/home/StatisticPage.tsx +++ b/frontend/src/app/components/home/StatisticPage.tsx @@ -2,41 +2,31 @@ import { Card, CardHeader, CardTitle } from "@/components/ui/card"; export default function StatisticPage() { - return ( -+ +Quick Actions ++ return ( ++ ); +} diff --git a/frontend/src/app/components/home/WelcomePage.tsx b/frontend/src/app/components/home/WelcomePage.tsx index d0b7f52..1960261 100644 --- a/frontend/src/app/components/home/WelcomePage.tsx +++ b/frontend/src/app/components/home/WelcomePage.tsx @@ -3,30 +3,22 @@ import { Card, CardTitle, CardHeader, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; - export default function WelcomePage() { + return ( ++- ) -} \ No newline at end of file ++ -+ +Total time spent coding +- +- -- Total time spent coding - -+ -+ +Total Problems Solved +- +- -- Total Problems Solved - -+ -+ +Sessions Completed +- - -- -- Sessions Completed - -- - -- -- Favourite Topic - -+ ++ +Favourite Topic ++ + ); +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 20db080..008c16b 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -127,4 +127,4 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx index 8800ab1..275abe7 100644 --- a/frontend/src/app/home/page.tsx +++ b/frontend/src/app/home/page.tsx @@ -3,34 +3,23 @@ import QuickActionsPage from "../components/home/QuickActionsPage"; import StatisticPage from "../components/home/StatisticPage"; import WelcomePage from "../components/home/WelcomePage"; - export default function HomePage() { - - return ( -+ - return ( -+ Hello + +Derrick Wong
+Ready to start coding? +- - - ) -} \ No newline at end of file +- - -- Hello - -- Derrick Wong -
-- Ready to start coding? - -- - -- Start - -+ ++ Start + +- -- ) -} \ No newline at end of file + return ( +-- -- hamburger --- PeerPrep --- Avatar --- - - - -- - -- - ++ ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 22b4c9b..6d2ab9d 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,9 +1,7 @@ export default function Home() { return (++ +hamburger+PeerPrep+Avatar++ + + + +++ + - ); } diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index a2df8dc..2adaf00 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -32,8 +32,8 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); function Button({ className, @@ -43,9 +43,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps- hi -
+hi
& { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index d05bbc6..113d66c 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Card({ className, ...props }: React.ComponentProps<"div">) { return ( @@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) { data-slot="card" className={cn( "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", - className + className, )} {...props} /> - ) + ); } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-header" className={cn( "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", - className + className, )} {...props} /> - ) + ); } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) { className={cn("leading-none font-semibold", className)} {...props} /> - ) + ); } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { @@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) { className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } function CardAction({ className, ...props }: React.ComponentProps<"div">) { @@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-action" className={cn( "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className + className, )} {...props} /> - ) + ); } function CardContent({ className, ...props }: React.ComponentProps<"div">) { @@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) { className={cn("px-6", className)} {...props} /> - ) + ); } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} /> - ) + ); } export { @@ -89,4 +89,4 @@ export { CardAction, CardDescription, CardContent, -} +}; diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx index 524b986..bf4214d 100644 --- a/frontend/src/components/ui/form.tsx +++ b/frontend/src/components/ui/form.tsx @@ -1,8 +1,8 @@ -"use client" +"use client"; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; import { Controller, FormProvider, @@ -11,23 +11,23 @@ import { type ControllerProps, type FieldPath, type FieldValues, -} from "react-hook-form" +} from "react-hook-form"; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; -const Form = FormProvider +const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath , > = { - name: TName -} + name: TName; +}; const FormFieldContext = React.createContext ( - {} as FormFieldContextValue -) + {} as FormFieldContextValue, +); const FormField = < TFieldValues extends FieldValues = FieldValues, @@ -39,21 +39,21 @@ const FormField = < - ) -} + ); +}; const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState } = useFormContext() - const formState = useFormState({ name: fieldContext.name }) - const fieldState = getFieldState(fieldContext.name, formState) + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { - throw new Error("useFormField should be used within ") + throw new Error("useFormField should be used within "); } - const { id } = itemContext + const { id } = itemContext; return { id, @@ -62,19 +62,19 @@ const useFormField = () => { formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, - } -} + }; +}; type FormItemContextValue = { - id: string -} + id: string; +}; const FormItemContext = React.createContext ( - {} as FormItemContextValue -) + {} as FormItemContextValue, +); function FormItem({ className, ...props }: React.ComponentProps<"div">) { - const id = React.useId() + const id = React.useId(); return ( @@ -84,14 +84,14 @@ function FormItem({ className, ...props }: React.ComponentProps<"div">) { {...props} /> - ) + ); } function FormLabel({ className, ...props }: React.ComponentProps) { - const { error, formItemId } = useFormField() + const { error, formItemId } = useFormField(); return ( - ) + ); } function FormControl({ ...props }: React.ComponentProps ) { - const { error, formItemId, formDescriptionId, formMessageId } = useFormField() + const { error, formItemId, formDescriptionId, formMessageId } = + useFormField(); return ( ) { aria-invalid={!!error} {...props} /> - ) + ); } function FormDescription({ className, ...props }: React.ComponentProps<"p">) { - const { formDescriptionId } = useFormField() + const { formDescriptionId } = useFormField(); return ( ) { className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } function FormMessage({ className, ...props }: React.ComponentProps<"p">) { - const { error, formMessageId } = useFormField() - const body = error ? String(error?.message ?? "") : props.children + const { error, formMessageId } = useFormField(); + const body = error ? String(error?.message ?? "") : props.children; if (!body) { - return null + return null; } return ( @@ -152,7 +153,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) { > {body}
- ) + ); } export { @@ -164,4 +165,4 @@ export { FormDescription, FormMessage, FormField, -} +}; diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 03295ca..b1a060f 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( @@ -11,11 +11,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - className + className, )} {...props} /> - ) + ); } -export { Input } +export { Input }; diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx index fb5fbc3..79d77b4 100644 --- a/frontend/src/components/ui/label.tsx +++ b/frontend/src/components/ui/label.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Label({ className, @@ -14,11 +14,11 @@ function Label({ data-slot="label" className={cn( "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", - className + className, )} {...props} /> - ) + ); } -export { Label } +export { Label }; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index bd0c391..a5ef193 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } From 91cbfe5789fd2fa2115c1a394aeb2545406b720a Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:15:59 +0800 Subject: [PATCH 20/97] Refactor components' names --- frontend/src/app/auth/login/page.tsx | 3 ++- frontend/src/app/auth/signup/page.tsx | 2 +- .../components/auth/{LoginForm.tsx => LoginComponent.tsx} | 2 +- .../auth/{SignUpForm.tsx => SignUpComponent.tsx} | 0 .../home/{HistoryPage.tsx => HistoryComponent.tsx} | 0 .../{QuickActionsPage.tsx => QuickActionsComponent.tsx} | 0 .../home/{StatisticPage.tsx => StatisticComponent.tsx} | 0 .../home/{WelcomePage.tsx => WelcomeComponent.tsx} | 0 frontend/src/app/home/page.tsx | 8 ++++---- 9 files changed, 8 insertions(+), 7 deletions(-) rename frontend/src/app/components/auth/{LoginForm.tsx => LoginComponent.tsx} (98%) rename frontend/src/app/components/auth/{SignUpForm.tsx => SignUpComponent.tsx} (100%) rename frontend/src/app/components/home/{HistoryPage.tsx => HistoryComponent.tsx} (100%) rename frontend/src/app/components/home/{QuickActionsPage.tsx => QuickActionsComponent.tsx} (100%) rename frontend/src/app/components/home/{StatisticPage.tsx => StatisticComponent.tsx} (100%) rename frontend/src/app/components/home/{WelcomePage.tsx => WelcomeComponent.tsx} (100%) diff --git a/frontend/src/app/auth/login/page.tsx b/frontend/src/app/auth/login/page.tsx index 861db1d..ccc9df6 100644 --- a/frontend/src/app/auth/login/page.tsx +++ b/frontend/src/app/auth/login/page.tsx @@ -1,8 +1,9 @@ -import LoginForm from "@/app/components/auth/LoginForm"; +import LoginForm from "@/app/components/auth/LoginComponent"; export default function LoginPage() { return (+); diff --git a/frontend/src/app/auth/signup/page.tsx b/frontend/src/app/auth/signup/page.tsx index 6d8ec26..1b9531d 100644 --- a/frontend/src/app/auth/signup/page.tsx +++ b/frontend/src/app/auth/signup/page.tsx @@ -1,4 +1,4 @@ -import SignupForm from "@/app/components/auth/SignUpForm"; +import SignupForm from "@/app/components/auth/SignUpComponent"; export default function SignUpPage() { return ( diff --git a/frontend/src/app/components/auth/LoginForm.tsx b/frontend/src/app/components/auth/LoginComponent.tsx similarity index 98% rename from frontend/src/app/components/auth/LoginForm.tsx rename to frontend/src/app/components/auth/LoginComponent.tsx index 5c09c0c..5211d27 100644 --- a/frontend/src/app/components/auth/LoginForm.tsx +++ b/frontend/src/app/components/auth/LoginComponent.tsx @@ -15,7 +15,7 @@ export default function LoginForm() {![]()
diff --git a/frontend/src/app/components/auth/SignUpForm.tsx b/frontend/src/app/components/auth/SignUpComponent.tsx similarity index 100% rename from frontend/src/app/components/auth/SignUpForm.tsx rename to frontend/src/app/components/auth/SignUpComponent.tsx diff --git a/frontend/src/app/components/home/HistoryPage.tsx b/frontend/src/app/components/home/HistoryComponent.tsx similarity index 100% rename from frontend/src/app/components/home/HistoryPage.tsx rename to frontend/src/app/components/home/HistoryComponent.tsx diff --git a/frontend/src/app/components/home/QuickActionsPage.tsx b/frontend/src/app/components/home/QuickActionsComponent.tsx similarity index 100% rename from frontend/src/app/components/home/QuickActionsPage.tsx rename to frontend/src/app/components/home/QuickActionsComponent.tsx diff --git a/frontend/src/app/components/home/StatisticPage.tsx b/frontend/src/app/components/home/StatisticComponent.tsx similarity index 100% rename from frontend/src/app/components/home/StatisticPage.tsx rename to frontend/src/app/components/home/StatisticComponent.tsx diff --git a/frontend/src/app/components/home/WelcomePage.tsx b/frontend/src/app/components/home/WelcomeComponent.tsx similarity index 100% rename from frontend/src/app/components/home/WelcomePage.tsx rename to frontend/src/app/components/home/WelcomeComponent.tsx diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx index 275abe7..eef6c2a 100644 --- a/frontend/src/app/home/page.tsx +++ b/frontend/src/app/home/page.tsx @@ -1,7 +1,7 @@ -import HistoryPage from "../components/home/HistoryPage"; -import QuickActionsPage from "../components/home/QuickActionsPage"; -import StatisticPage from "../components/home/StatisticPage"; -import WelcomePage from "../components/home/WelcomePage"; +import HistoryPage from "../components/home/HistoryComponent"; +import QuickActionsPage from "../components/home/QuickActionsComponent"; +import StatisticPage from "../components/home/StatisticComponent"; +import WelcomePage from "../components/home/WelcomeComponent"; export default function HomePage() { return ( From d09b8ddbcb172e3961d1ff814a31075d1743cfb7 Mon Sep 17 00:00:00 2001 From: Derrick Wong <142132416+LemonDrew@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:51:21 +0800 Subject: [PATCH 21/97] Add logo --- frontend/public/PeerPrepLogo.png | Bin 0 -> 106022 bytes frontend/src/app/auth/login/page.tsx | 12 +++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 frontend/public/PeerPrepLogo.png diff --git a/frontend/public/PeerPrepLogo.png b/frontend/public/PeerPrepLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..fc533f8ff6dbba6e2f96380153c84e06bbeb89e4 GIT binary patch literal 106022 zcmeGDQ+sC57c~mU>9}LF - Welcome to Peerprep + Welcome to PeerPrep h)f~16q5(o$w-GA)|)b~u9R3PwsfU%d*bOHgvAp5U@ zN-B|Fe@}usDTxb#)J)@@em_8%3CaqBfYisry&FJ+fM$0}iU_K>f&S};&eIWH;_We; zroHM(*@inMhVh?;KtqG045QTlnXDUdPa=xu%L6GA5s)t)k0>M{^3z%Z0-~t(XM%u) z#IJ|Lho?(kvm;;AD_$r1&)zFkFe;?S`Rn!W;~cI=(~IUOsGmO|!2AV>{_pUAf5HFt zg8vUTg0%$Y&mVlB_D}83z^v;+xE76W$A_V1XDdWXmnJ0LZc`iQ*JF6fWa+(vWe|K# zg=OmMYH*FpLWzC0c|uepSust8c(oA?1~D?JKv}&!L%2W1xyG`Fzn Bmt)=-$C)8k{3)?7bv>#B`XK<4Qc0Hqx86|GjR)baZi>9nTkEyJfp3c$t- zK=t=u{jg@T&4dNK5{GX@acc)3OIF@v)^j7A_IKXJ 9m%;8sdBs@d>Nt5$AbRkU68-O*BuU2Bd z$FUWBuf;=sDE9{@V%WL^HT|AMgU2+HK$niU@pN2no_!Ztz~lb(WmJ8Ybk2>HWZxEc zza}o6`E-0Goau7DD0LY`ClTP=v|%RkWxN@k7aegDQPeB@G}SHAZWKi7@HM|BJySeU zgh}b*nI=H~QFb4dc27bT$h}|F?2>zM84d8fbdlyC=#Y0TWpZ4hoS*malt)FE)~5d0Ife-u->RE)uie0mo; z*YOC{u9xfhZz~4f=VAnUWwH@Y+_(U_y1+8%7{{hxE)lT<#K0Vehrz4W0n*5nqP~5H zG7m3@&z|miT9}KI;p_Z;rDN^EO1t}n4S)9NR#ozCPSR$ZFXLs2(mmFnC+zn|p$X&7 z -jnQJi7Y F$r>V9L zxh-wQEF8B3cU?Zv72FWfbCIlG+VEa>pHy`DUFDPtz=s;2Gp@_~t-_9zexTk`j%HNx z%exu&yEuaZhTWz^;7_5!LlC?T{sq;$t{| #9!2+#P($aPLWiNDwMs{Q9RdSB7x4w`&wB>&7U ze^-q_@BZB>N1hN0`nI9_pkPj!aUlQ_%{C)Oc^6%vBc7m=8-2(3;(+i8RG<`ZCXd=~ zN$;mV&e8NQxAhPn2)*if7I0Kmu6-}f8|@M^n1pL}`wC^T&Y2dSVv*7F Bx_g*%V6#@?@ULc-OJdmR1q5Jc&O zX+z^Zx}0irM8F;6bhvF!j6JBd6a);~5F?KP@lk+s?3-97(-sjyKtZ@;W8L9WuP0~- zK8ff%In|e-0UoTGW=2N5n|YE}7~0MGye0ulJ^ZHvJ2%RqV#JYp3Dp=pO5j8oaX>(k z05kYrG}v@cTLnAXk-&Q1^*(C_7ilNQ6eRz4!_iKOgN^xUy#F;sl_oz!6&B t@)5+yni zy=Dyj?}UJs`6Jii_BuOy*?!las+Z26XF8B%h=%FJMz 4w)A^6BOS`ZHO)b%p-uO`|_%y&i>&tUV& z4ar#r-Lc=Vu?5c&x1Ey{;4^D@w +5iG(=PZRh5gz5hB_pD3?gK`Jue>fbm$Um^H_F(*~aIud|(BHhBKT4 NHN?wjEdqQ*VB-s&hJ@nqHMn3Dkc*XNI_8ZONrkAM zOi3ejNe5E3P$o9L_fH5yfmIZJ0Arah%CMn&p>1IjPM_Fnp!h-V#BK0MDp#N^R7Hi< z)ZO-24AyL8(`ajn&)EvUmD@N!F40dfc%mWEYX?xdLkc!+o@ak8TkY%V3Fntr2d&ID zXPt@?GX|wWP#naYHuv>7B$BEQT3-i6cpna$+7p{Y@%Sf^yCOr{QK-!kl#KqHL%J+u zc=>9OPosfRGGkjv{ldTppk|bnhFuxaOk^RhAA#NxN>QXS$Iy;0=3Ek7+5Bu#r~!Ho zo;Q4ykR%>+1D{xkwXkqHUY(_XCeh)TqlB2}a2UBfDg?RjJXpN&-Y9{3o>O!_dg%-) z8;2W*J%$VOkj(`BuAZTG*a7r#B1fh8GxcDjrvghH?~bD1aT0Zwm2?EUo*9WVY33aC zLNS7%07@|0xJJk{=B>OT`7)3+rJ2h#OCf(V?lFI$Uy(y1Id%r9;>JMpLc9RJ*Q(H8 zq{^{T1n=>Ux*1d|!jtpGEz2z8%CXPen7&S$gZu(X-_8hu0}&7lBbUL)`!(kM)bl>Q zUgH((pq1X%jvS4B!bL=;w`+-YtUB1pbc&9JoreWYXU2FIV5|K1>^iaTFb#1=@-#(; zVc3uZytni(VFQWzR8cUxZ?O=TfM?
?{Aom#ev(MtWAfc!`%~GmJ7*D*H=E3xOwai>?#azFAiX2nNI8)$ z2}90zp16ev&AJ<}MZf$~D4ImoJ`zu=8Ihx{mafL{V9*y>RKqgA`kZ2o&2ry8R6ddM zqfvw0A`sGsAcWrWa9Lxf|FW3=$LZ}EgOiE2@rUJ=x=)^^BuhK6u=CLXg0NPRE Rli6`jDoqn(Z#Tu)9gW(Hk2j)F&( z>QJ2@+g@=&Ub=prpBWM2*mZ?2G^jsJieC3Vg!wFk3^6&I_I1pH`>g|&S`>;A6&-4U zRh#T4sR=SCe=KuEf@5HfO0 zt_~JeYKOVX=$ULf5oLsV^v=!_G zAC|;lJU4QZb7&{Rd*C8HNmM{|ELiEac+>LXM2M>-HM~8+;t;Ip2TK%PdNot{I|5kh zIo4(w<9GxtyMQCC ??jQ^8JMF)1$eRU&7Yh1Fdm1B^ ( z`rB1B;s-Y%8VUL) j^Lkq*=QmPfIc%cwTFE z*t)RLFg%BEi*sy(aW$+fU5})EkoLhD5t4x l4 zzBk!TzHqYGK~fPz;Bef7Ufp=dJp_wFpm>7>qz5Z|Y+Y?)8!t&HKEho#DD%k(-IagA z*Va=`i<)*EJ<0{^sCY$vN@* 9?_LoyKqM zMQ51=vY@1l{3Qei-S0HK?9idw)&y^crAS72_#n8~pjPjNZn9r7Vv;T#!)tngj&MwK zt1d%y67zFdboN4#-F&jVQ7?l#uSL(-q?0S_l=UN3WVsPayS$;qfR?^vJlAg^66Oz> z9Q<=-(8`DadS6)`Z7>f}c(rukaFdyK$J>j_%{pKHA;UOAitcDRr{7EWM5wJo_zpur zqM_TK8Y#F#Sp+ZJ@`%P?^nGRy@XlT`07_J {iTh3k1dg`E2mbwkB zzEUnlWURji1?L8973XL0R_joIN*!ELKNZ9|PWBpb!)k?vV1Hr|0pQ~=8A)l~rVV@e zK=Snoy-PYVWIAcgx{&jm`Lc5)D0Zgg*vYoVV>V2b&KgX0aCz>AdTl`!bs)CzkbgjI zVRAf+aXB5|J{I}5R7kyekn*Xky0;JhCaiFzI)A{xfS~kd#Uir!kcCpcSb`u#4`}#P z*qz&7>qONzl#kqUXL(^mR@L>)LP``EB<~GcQ(mANOcm%tkZ7?gyk|s$HGMMzj4 0M?0rNE~oVykry1L4y!?1rnhtI9VX zJScr4ynAaMfemS!z-~0zgD>-~;>m~l-`k7ttcVX#u&cC;l6&E`aVaFswf9DJz<3sj z7lofdE@4W2ZuCw%=0!`Q3vPqhp(e|zE6ms55Q4z!pb3#L2~1r?GTB0Zalg^h#$Pyyj t`FX8dQu_~wni81LOO=lAfDL+R>_m`6G%?zZ8^<00# z?ZuD(peL%7t{TABauM0LCDS$dl>h@^rm}4ph%r^SSjA0%uh7QmTH8p8U56qWz+23& z9b(4qSQ9?vnYS3*s-pePr0?#YF@9KZ^vLn2)9uus=qJF)Sb*qcgrTF&*T~69wq?bS z17X)Z;f3}2RYzrd@>&Ej=>?3G?zsv9^D(?2E5O=Vx>$plH%z9Ez0G|%W#xvWKaQ_} zGi`T}ZSfiDYnq-VNm>>qVpu(oiljds=sf#-g?ZO4gpF~g1x>MkldOhguqhA#HS_HV zH-ZTOrF|Ld9nQG~m%ael27 >}Z{+ zmq1lI${;oxYVWAr<01OP$CcS)D(7M)S=O#jx4D|r6naqqyiB36$UH5hLvCqkSLdNP zDwoqSMKr`ebr$F<5*kp1_+V@zl)_U1``R{03ti?0YM2`;y61AOv(u+zQ+T$U?Ej92 zXyXX`Ws#)rXeP;qDbZIyU`~vzfBcB|BqN<^%K)6#a8U(* ?9q+YF$~;K`EA4b2{i z5oktLVKq!8NF>|Wl11{yY~^=Uzo0ZH@eh|EU-RKsI&Cir^HUrW;XIrhC?lpD+^CdG z;Hvg$bLGP}rUcT}#ImSuFL=e{^D|{+)l4w|C`#@dL26A8dc<^vluUCOjk44TGSwKq z#P15J$|-~WRkhEj2v!ql8uTwrd3xT}cpmO@!P84x+?8_ugO3T%3f8~3;e+F&pVJmK zd-WQ}%M48WL(BvXDhbbb;`oj>S~h*4ufIY8*toc~7L(oa0)oHMkDGj##i2d1?SNs5 zvXWP6hSX{Ka;sCy#&TfF#)2kDbn;)2 lKwYs;}H9sWtZMHADFJy-s?S&&`*-O{0}WmnUINw80Rs zPlLJK8w(eYpeGWo=rNUzd3)Ov&ok#Tt%^%?@wUIbXC_p4wj(#HWm|z#dRt}`6}{m! z-VeFwJ!-bx*(0)FJ|FW_MH`a6E+tp4qq{)3xJ&pde<2U}OK)2}kCL~&-mB!#rP~?r zd8bBGvhGraEd#9=o@Z~5nV#a>XrKj5PluPPGZ(G+$W`u!OHak1i?d5f?#f1$R;OBL z?WLWM-lr3=V&SPtz1^uB>irY*d6Qe^l3xd$%d}PRtNlyzE6J?L_wntj#*Ta|)Jruv zT19?qQ&(ow*-|G-H&pV}U2CzzqZF^KyIWT9>z?=YV=hCtQ1$o1x$c(bxutl;ZT1AN zTyIO$^6K TcppD)k#PP(j>`qL$& zrFB+AD!f}N>n%6XBw`ir+;p|mkISpHz|gr_v)fTNA2a&nTeNJU{MVtk9mPZz^DHUd z=-hBf>~#~bXOamYF;+q>1J<%s;dHBCmytv9-H)SA*?XR!fdWLUGII%^Z!fzD2 j z*pA Rd2XKl~RS>`}HLXV(dv> )yJX!@v}EKn7_2`0mt+#>d1|#-n)ZBkr^~htrM3 zdenG6`E+c=+5yQOF2(B}$Julru{M8x{$TShtDNCI53Vk?{-}* {6Fuu8d@D#b=+n+zIQx@_ft55hbs`(j+kP7m|9s9V$*8` zfe5&SCS7G!ZI?sER28}Ct>0!aIYPB~if*OS!<~&M5S987nYSVQeV}#*DC^Tw MtI_}yBzYrm0IMX;PheIyKAMS9vx`QZbGdKW{CbUHlTq5g7F&3H?y|Fr zk}kj&ew-QwV5Pr=FnD;uSvi&}+1kU#pDa$lBJrfEluYL@FzX$!tn!|oMMHg0N$vXn zh(Lb|sZpVEE|Fg0XlLP{*C~jGDcBnFI|{0%fHFC2@YY;gHtN!rT38$<5_QhVo_rW^ zqv^Q&OVb&)gTBh5Ub3JJwh;lLk+FXIY5j=AU!yRV`iEhPqL}Jrf$FRc@sQ|jH}V{< zjKi-=roJ=S SV@pW_);ZAi6@DA*3#rgIa6^cwvNW_@T 8!x%%f=#}1}P;V9t$y7*Bdj2~pYxLtqajmPu#zoz@wJvWbvtwD8 z^T4Y}BFYOE3CA9emj2A=LT+J>w1zm+R#LVjW}?6N>~03)w8@j_fk@8H$*}!?>dD*q z9|(d!U?2i fIryihScwo{cm&nTS*Jf+Cy+; z-ddtpWf#YzYSiN^l~UVsmKg!gW2^KhJ#m@pv(@=?eF-Q@L;6a=cI%P(B!vuQNoQTF z4D&`8zL7d^ho3-}XTou~RB6;MJ&nm>jQS-P$~Fm`rg|Vpu7a?n^G>F-Me>yCfi_$R z?T{wjNez*2b)sq(PfeF(Y6rR^7bP2I0bTogeNxg9DGV!QpPX1gba+SzRQ4zH<$1#L z$(?j6%{r&8PPbJ}&y%uI>)L0|J@u3Ua|oru75M6(_*X!?eVH1cKm-Q;;L@>|Z;eWZ zOtE-n`I~4t+9W3iKx=H3_M~QqgRh*T=7AP*{!h6=DI^1eiY7cdCnWhtpxSCMa_LA_ zlhs<7z`Ud7;Nh=J&z;LrJ@1=`eDVqZifZVM?FRL&+|T=k^3kl<$#f}i-?PQ?xxeYm z#T+}nplVs*$NG{SRzG~Bhn)mPClbaXwJ b&Xk0^CzfW5J2Z#0_>Ser! zQW6Z0fhe5O%XHn8a=i`}ZUfa=sOK9DTo0mZ*| zY+E7&T=MJN6YOrRGS9Tv{MS}v**Q^o~hZz@C!R3wdb?TH{s z$6_bZ_5K&Wq0ZvGxQKuYQ_`q9dSMfPaW*zmJ=HFQBEE!L+6TP!y`C?e;czAWVewKs zcBC|KYsfHdm&p+But?TDi5d~w0DpSv)zmrbDv-PFpW*+U*|KWEGLcaipP-!lC $j;|J7$=|T2psJ!Q zC_@IHD(Be9fVh2;LHhZwUc{(?rc!k{5b0)^Ev~+=oDcf@fLe}sz?0B|`*Ri)Owjc| z%y5NNU!2!?#KYaCo@Dg05w nT3BjFO$V637Nk zroxp%u1R%;z#hYZ{Q~Lt4GzFE`F$aRp~{y!riHfk9KjJ?Nn|7GG}LpdkU2iqP5=LM zr!dsOS9T1Wo2mTzpZK(5j6GK=*=(S;ePd5uS|8HmU(q1Wd+n()UQVBz;qAH~%6n;X zT>&dY>p$tG6q(dk9>&N5J)_+aA->Q@KoTCYE_9#<^T$>ZfUhYwCp%vTpj<2-!wzbT zV&74q|9u!@mslqJIPLTA8uOa*tVl-E%dLb@(CW4f9=d-&;@*4*!MgLLF{iofOmuJ$ zZsUK&FQ;TW{*J=qEJ>ddD0iTRC+HZ~^L=PkR`=WK6*4F_+s~lw!1_56iEa%sSTtdV z-oBDKM5$(gLM1?PG7{&nHQVjqJ_E(UCgSX*BcZ2E50a}qiGg)Kj)3uzn(&1S33v4O zBj;=d7#vQ6AzJwAo7N?b9FsI5f^DVN3#WBaHw6^D?(w@JPR>U?>nQ3x?sN2F;ceW+ z<|+=(ILEQ5!Ph6I1B`r&HRb CSLW% zsC@?>y4h1$cto}ABloj~(cvsORbI1hry5z6_l+8Nl*8XK+UOi*AnTk`3LXGJldaWt zE*bT^Y4!5+Mi^yxKHHjWvRL?REZ7 !1(x`#xg1zx>gz4j2%9Nd*-q07+G}^ z6>y2v9t^pK{#OMM@PSsCR}EsHt`NOT9Gs0+52^Etv+wbH Nm$TR& z@23a#FAjFojUu0WEUkr73KeuqYuQ+jlkSHk Li=@Z0m{On1xxpPvBrG 9VM`tKB4+aa z KsO{nsrMatG1dBIlLoY^_2OWYwyyrlz}gOVs7suOmRQ z=w7knF$<2S{}wB1i->c1YSooOQqk5ZusDaXrM!!t;&K-|*5u*-N87c2Nuook`ymb@ z^)0n-@=NcMKo7Id#gFamEk4*gq(pkTCQYI #B)9B2VR J?H5&7>^|tLs_1Ul#|N9GN*M(6+0t^fi0=1Oow5XU #6gsZ!siS{*xyKfaSx zwg|>lSU5`g=Ol k24JXIdUs`)}XUZ;@%+$n;9yJVU-#A&Q;B(^h)Rzvm zJJFtFTC6|DZiZ8j>9Ou(fxySxrt{jFTQleLaa`rWx~)dTW6oiqf{Ad0PzgO^Axh`C zWu4(9I>70Ffcgvu_{RmI>+>pz WKk#|s zhp WjdiM(Y2bND{eLiXx>oe&r2Hd}H_1f3&YPq~3hmm_Bm2 zNa2jnx?JQpbi94?3_q1<$%x2s=G({B0G-EjRmk(kXtu-I#fr?AD2rnP6-P6FI6kZ7 zF;#{1ekt|&WN&BrM6$=sQ{9x^)TG>+?L^inVaFRBE(00X%RIK3_8;c2rIp?kT#h~s z)WBQ~MGzs9-KP9|>O &8LI_fI60J_Ur*?3PQdQGC7 448ogVFTnPtjb z1`E+?%sSm*LENxkZRLy0O9V@?SVlR%r_HoqZ0F<0G{cG=Cf7rGiy{bZb(&-)= ogrk{!09ywr0C(oyD@(a-xhEnxK%xckR3>Pt} zOk8f&izP=qXZQVg4VsAGyy}N;%!o2ql;Nt;V6IrJEh}qiGY~YPe?mQI(SBKhE)4|* zf4Q>cyBb!y8wkEH_)SKRCdw&%?Yw|qN|suUf6BFUEH~f4$Tw?3Ehm+dHJf_SxaaHU z)QWw1Kjzswy$xzht7}76ru?vl1jhd?UT*F&ik*HjNCfd=o(E&*wG_szaJWVl3Bz^J zf7fhvkiA1>C7@FOz_B<}5(yTvH?fOq<(jR)v1*;yAZTBNEo3`S!CVgx?Qz=E6yNeb zyw?*eY^5~;kn@uNP3c!L^h!2?4jfqb#A0J|;Idg#p`g9$LYKv1HU6yWl*?p6{e4t{ zAh(Fo&G_^tna>tIOm>PMPT2}~tPTnIT*z&}UTSy5j>2NxORw~G+r!c5RY1Vus^NP3 zX9DEAXrlG3MBJ!vVeBqgyC)S1*%vXl8Ay1(tl*ABWt9sls%BN@cF3GZmFN2^pZQmR zmDn|9E2_^>Nf`eeAnJySMF;Ok_66^$rG2s(Zol3wD#$h-@onb+g@JVZ!R?e-uuse( zl_O{mDrJ*u8>q8)vfnV&H$u)k&8u0K>-r4v7A)9-0CTe;NT*pCvmE;;=ao-#ZV# {iuwkS<=_Hk=!vY9=n zYg9j=N(&USrqjzpK~bQK5RHdt6_*9}saXxrC3|;TYp$FbluPBJYdAihA%T3CtKrVW zOg$4d23 m)2qgnM9NyJWt5R4vg`(;@s6LHLtC$6c4|Yx zw=J$f-u;G9M84gqg{z*=j>-n37TJcHx=vwg1CGM-f)Z(!gDq@DPV{wI_4JCfB8yY* zbcaDt8aJnD=3l;^<7>2a@i})fN2*%%dJR?TJw2rjk^w#1D-anQAO-joQuG4qR;l=W zmLg_X21bN|NRnt-2?t)>y{|WGPQA|W_4~KglQL#j$`d+HJK6-Na#(|!vOUEm+&Xj; zX-@fxEsrv>k{hJ*ydPIOe7&PR@`CAI)}Ig~MNbao-I (s>E)$RR>? !C=&9{|TG@475CQEw;=gX|36*cT=Q1|TV86FJTwA3+)0NsVS zjlVo}3nj9Xf72KX0Yg;$JQpdIwa=n1i)@nAjry{z;=Ezr=2uMQs7$YM DGDW!`Un;603L@kauxKsGS7uDj**`e}izByCiDM;KutgaR6OI-*oook`5IU2d6| z$>w7`;15F3XWiKqwyegTASWF1Ew80}PJD)e<;9iun#HV@Bx;(SWNK=6!#XJ~93g9q z$TJFS9gllG`HQX%%qO3-8*zel$PQ2k$pjV*w9a`BMGqgRmTre+Ikm1z%kzo?K3>mi z0zQLCcW-E)S0vU4Z{5nXjl%ZsMu+LYlk|G$+i&Z=goz6A|A)VE3vYRxo7kMLsQ>nP zTDb=zZLu#*znscdrqX3}S@#cI(P}qY??tHI!76M_KH~7=bf^V?f@DI@$A!8S9i}z< zI{uaI?K()EE358w{Bt}T7D-HR(>x-gN0eJWyyrNA41T;vsgilbek_J6ey`VdZq~!z zv)g6CmOZys!Vb%6)%JF|S$&+ P0oj zYFeYQ{0~>zu5S}Q@;sl%Z!VC8n(H$03u`ohCV@#UyK $eOmQDz*F&wOiu&y~vu}=D&Ej zCTtyb%+{_~!^_6b%g5GHf_6Lk)hLRm3;w&}QMKwn^;>2=ZO6Q96&+`;>N?MxzFi0W zPg}81LTV6OINw4u7a|CB^F_3m^`eP>I`d6z@Ic=CC{Bz1?PjCS!vD=hX7J^0(`W4v z3GFSmCD?0QwrOz+$7@%464IdFzo0h8(z>Q&wddfX!hqK*#pklEaRlgbly6f9Q#aZJ zz5UwS%jaY7U12K7D2MF_yVYJ!%xJ>Z-18xKzm`q0$$UQJn+1#Z@j9kTFRvTJTWJuw zQQL95Hs8nS(87gV{1!Y_MX6zI{!~R@Cxgt zEg z>)SGkpQ`t6EZ)o5(h&!Z`j!?lNzOKLrPFRf%B7Vwp*Bi8nIb-DRhayi?KqO6p5-#s zA?v`SjEmzKtLt<(k@DK(Y~dztG!bK^fv5N-e4j`P<;d%~k)ocf42|!;YS60VbV~cp zp=<9P_@8ecTXnr3oE(7h >{@?SQ~ zK!VkUO2h9PPHdc{l@izC17ANgR!J*QG#gEcEjZ7+^nA{0^geF&E_lV$&en12s4Q+4 zx*HAJ{rK{ z5YJBxoyP>Un4OW^s%JgSP&}@B*M1O~%#-XF3)Eo?5QzBe{|a18(Y$%0@3P*jt$klh zq-71X&R0jW>WqN(BKf)e2uYWOwAs=ZNBb+2opur!4Yh)?eBp5LB&Lq$ut7>{ee>=P zjf3jX$jBO205zo1lazOd`oLsl7(pJ-Yhe7XuK}{c z*s#jeu+12k+@hUXniqy>F;(`Lq@<^3iRo?3O;zug)6 W~UhXe$AP7K3AdithUr%v!afHflh3X?9@Yt^GN+w zIy}1>DhW!|0$q<|s1|+a!T5+wtc>%O?lP%l1*FCW1gN@05ckjg`@LtWqzVRy*Gw5_ zVq47y7VbLgg~tcs*zMa6IC(wT%($j3htpzKc0G@e2 h733e=})tnhR-Q^9fg6d42 zzaBBWXVy-P=)hE!O`nr3eS}n{w^p?8$Etvj2{Xk(WFmaiRyJG~mQ9W3BIkZ~T=nWz z_tU*YJ>7b*Q;bSA)-xqfGp?C7sVd|)wn5wWEV+xgabug>ZC00^9pdQjH>78AS(^`T zZ|J8^y;$s5fRwS9>C_R^4iuB~W; q0->a)#A@r)|(O zMK8$W5{6bP-OQu^@^ZO&HqYrgTyHNH+t$tQF@eu5XXvKKMU<9h%ldRii)nB>S3h4C z$3#GWYK>CqMXq8c=8U(k-_>=OgJ98fo<(V{{S$(^Uhq {vX_j!$UjT*TsS1$(~Nz z!wNww_1a~88+;AyF)V89!m`1R^XiL+x`ck=S64)Q-diARbGI%blh?^rqp%0R^Wiv! z>&pxnF=K%2f1wCje}?OJ)tzhI3(ua9iAeFDo3nrP)fx**S~4&Z6Xs!O9% =>l|u63jl;DWks~-vL53HCl#)=*Vq}%>(Qb?s~=}?`N50 zuZ_7z3l&1)_It-UX0HpdSJ&+V*LhG(V`HtHBQi{C#!~vGMq7@RUIWC_`^AZ!-wSvV zI!F%YvLH7;(i+42Qk2eYDpQNos#8!O0oT0f;*C*fOhBcb#Wv-xbLyRpGCIAio>RfK zvjObB+}`kY+WgYL-%h&Tw(&d|##~|TZvR)uI+=QTHzG~%-Qu95m7g@_Us2KT!M`R0 zRbU ~hvtuDRBRS|I}ih4>HH@Zhqs#c%vC4XEw;8T+Yu z(=KEPfs6kQbGWF2&h}>$xW7$vpa1d`a+ MBkAb$Dk@ZA$ZeQk9;kA+4*%)t0r~^$ zu!G`S)?S?GyKi3$s;xq846HZNZ?@YC4O ^+aRlOUFQ|t&` z{;4g{fA(|NoerX(ar3v89q0ObA5O&d{3;w%wXUC787{O9IsG(J1N5w806m29z3%7U zHtmPyzaTPy=2+u7-fA9i`aCrAPsVOc7;7W&n>5uj%EZ&j(zcX~lcF@-YedNoiWPIt zn8fm(XJyy&eT9C7$RYuA`mzAq_pfAjGn`kaqlEmnFSo963MY^+pdX1983A&GhGbgQ zrm{E$%8OJ38n*lq zcV4X9KP{zh>C__$=|bQle3yyhLL1n>T}qnyeC3B;y)5h_eL3eop6RA!>dL44Nzs zEth~>B@96smd jw@^4BsLYFkTFSva5?a`jq~ z8p-In5e#tAo~}jQaxT11Gi~* 9&O9^%;$DJl=i&+LzVX_G+q6X zm=tlidF+_Z1K)wx8Nra(Q|EG<%I~ztjKtq1nfX?HJk>E0<+}ZRKKjUg6trm-`@Y{; z-e88lWwt23IlC##uZR||QQ)XZfAS9^IhOmi`|h!;Q-1+0mrC8)eG=d0Ydn{*?fCOJ z$IEs_$y@J)(K<5eS`GL740Kp$BE_%^2!NDS6DWw}=2(%(ZTmpq3P3j1NLQ*bcPYjx zQ&V)l>dEtbkW;JD*t%Mx$mO#?IH8T@yACT<=lMD?s#W_JD^S-@tSK6RfP^`3AF`OM z!)uy4bJ2b Trng z)P3}h#J+Dd(|_-ZZ5?Y(^4CJrH!&nO&)deRXSTMrB9jABSGUe _V5Fd@2tdP zMenK*4EN#l%G&l#TfKs0m~C7Gpak-O;|ScTIw=ra6%1whl0jU@#eE3t=RNYhYYPmW zGPjqSJVMP$dmHGD4fjB(s~PH510-MjUGGu8`r@3O|7nr2;k1}<{1?A^keF%pw{1m| z;?S3W|CaE=b%;1LMUCFoJF2ZYv9ng=$3A8JEn->{&4yA!I4o;Ld2uxX8(G`t6{FeL z`$FR}o%Z9~#?`ZflWMKZMVkX#W^{v2HSM3`2wgT+B7DYa4xM>?(IH+ge7$fb14tti zk(7ndnZun@!^n|iYNKi=P| a#J>t3p59};(Xv^jm4g 3ffmuN-HY!B(Rm!1nt{O`Ls z8E9;9^93b3_WgM;a2OX!rz?a~#dK;B@VOpEv2MkHB*)nz;b<)GT@;@yQ)HzRw`L@2 ze(y8S>ggjZMdzJ5m}W2#Ejh-9d&1x?x^g&IzS2sA1)?Hr@8h&Y`fi)onJ-pX=X$-1 zAS!aEdY1^YZBa!Layv_^aF+B)2vnN%o`upun3UzWDDU4CnC!Q)sNTZ04qsV%Jg1he z!DnD9vDxN1?lzx6owXg7{kx^Po>jksi6$cT!M`&t`ohFRJUu?Eimh9K;o@G43+DCZ z= x*t9FVsP!a24hyK%P)28Y|2q*dVzX?O|Hw> z)+ESt+6C?K5q3m%z~CIM8B6jGG5SL9xJg7IE>!Dau75|yok6jb#mBW`aY+vNl+8k^ zUh9*8C=h(l71>( y z!)n`UW9t|FgJcEa|Dov{94hUiy|Xphc9SO8WKHJDoNBUd+n#LOb*hs+*|u%h*L&~% z{)J~hd#$~GS_fyRmlt&pJ(3$9P76jkWvdn?2`*extOuj+F=}x)CRb9mW~25xknW-N zXI8?bblaTDXM3y@D$(pA6h0B;jp^{Q>g;`xG0t{YWw<5 gUvmKvWEc`RUde7 z(PT2N#X}V0 6pEFi^7RETv zxTteG8yLg;pyr1x3pb 3@#JXS(z-hyMF=&YjnHdWcg?9ZvKQ;3KtZ`;VAO zxBcKp48Je~^bN+m!gsl m}jtU J}h;9)l}LkK3!9x!Wcc~QRs=-!4~6o!9QjN7;%@doOKFpIZ$iHfI82dVWu;Q1PA z?^qaAsinwG&`l!Gmx&WwB6GaX%J^MhcQz)isZVmZjQ)Fm$727~V03%iFwJ(mIj%Zw z-e8sg>V*?`hvXE43KOl;lG{NWA`~{b2F~KbESbO9r_ MeMreu}G|5aLfUM`?E!x>rd^0U5;n)TtFO~?vY`lD&TXfV5e(WUu5 z06@gx;;W4Of %^{TuDt-(oyoyQ7u_jpT)~~h+I^-O39yD=7Zb zMznWfyjgW_k?yvkoEjSga-ZQ0q=gn0iN#!clQv!FLGH1i`>5?e>-rocqwZ|yYh>q7 z;9<2o&UelAf}vJ>Y8zWe4%|AS?A6n-O9DM`p{s09bBs5N5ccd2!xVsQ5%+G!z06|+ zN+d+Nq`~uck6&@*)l6atkhF$yEkv3~H%(7lo}T}s|8~{au%<>Zgn5Piv24o-sVxtR zgs3(BNC1}jVQ7fh*TG>uWi`dh(h HJpV94VyS)-s;BPk+ zefdR*Xy1RdK~S9YG2R#32HGE>sgJAay{~%JrhnDm567)`@lHnsLEJn$9ZE*Ay6Sn) z+ZJaze;C?ef#}qlZnNJXkCi^ZVMk;|jL)=AQpziG{*9?`ZvCBx&+76|-}fdlF~bm} zH|uuMbmF^@=L((ahW9LT>H*EU#%RC&pl6;gg^beyx8}rE5A9EsA=7->yuq(=h=0ZK zrWIO$^|k>ciGDT^88!Xx^SS#nJDcCvK8JXBumsVc^Be&iA53B0AEQMxRK<-jQD$^BwxFLH zGk5|SC64gC#~ 3LHJSb^ho7(P#g zD}?|kWRYDZ<66D0JLE!Q6l~gG)VYXW3nD==RfSx{?n*RSz9UC1IGt?3a&?klQTI6g ziXYetFCjmti|vE)1q|1!YT>tkMe-eP3Ou|HZN9H=>Ln2I6%_&_@cQr$FF4ecLhv`@ z!1RKJ?3vU{#>4r?nM`+pTgs1-L+Y+doMzR&WiO_q`Z5d3RbZKi>D)Fiu<6%99$K zxgX-E(n?$qQW`OUwu;v`B8B3M?bCPvC=n?1&PtS>5`{IRuQ9{2)iGo{R!;w3+&>Y6 zm5YNBx{? N+6a`)q6Y z9~e{~yUI)+9C=fjoq(8Xs!W8-1v718y>IHqu;CLD2nqfa|ITM?p8Bp$-@h_ue(9a@ z*@E7a51JV1)sw|Ph7&sumLRY0N`Nb?_qB$Bi#`@}Q=wdGi)8#mF^#fWE+>ucHc#X8 zZLF)*nzeYUpTo4`En(CuT=mA(lboD!dq0W>pKtFtwWD?GKGnOpd8rts#0me1LOz}^ zk?3CCr+ra>I^+Fx_A@Jl)BHkK@fP<58igHNB9yX;9H|gIZaD?T{U8>P2e+oJhR0TP z@MF3)El5iwI)%vGo$h54NV3b(tHAQI$&`w*iWyd+#i)$THP2O|*;xL=-tadUGUfnO z OBmV&nZM|D%HUYIu%>tLpNYzry zNk^V>!X%V!hI~67E^E;`mVHU7R6tK28Pnf TVIVvCvn3Ml}|5R z!QYVbtpbV1ldkJt{Wx7O$?XfE2dCr-0M+pat`_%dy|378wOQ0aYlCsV6=#jid&u%? zP1niB?!?E4o(=VKx5Llu$mbf62k?ZiJB7EsHHx*3Jy3f@H`;4s@N}WO_F8-V=ks1- zJEyRUILDXRF`gnUG}GaMwd?cTwwgIA+qeNuvWz=a%6jQyv_5aX*0s;BE=6^6s%c*8 zH2`{f#3=Op`&kdHRebhpb@VSSy9dG7L&3}I!_D<;J`T5hhMUysi#GBUg|C~kzR&>Q z==+?J0oR`2PaDaF>WGXmBf?{eDjJ_Mf`VprE%0Nbn(NR(Hlmt`-&Z(|>`zc6EY6Ta zBs`Hx;|1+qeXM}pXMqaA#YE2_k8eOM&wu|vWodK2@KC1Hg(Y(rZ^t5bK@>yhXLfKh zIaRQ?&l7=Gg&|z1N{&Cobqe;?9!Gav?0iRUfghh8+4sgqsPf)F(DM7L+oGAa;41la z`@J50WYYwJ6-xF0%&=UWD_+7AWnw??FLQWLUIlrBXx5NhMJVx3;$bfYj$}N~Z%Yo; zbH+ m&{mdtVe$Y`UM0 zGg`bL%P!6h-T;)=swU;anA&MOY4Iv}wG1NbC@%fwAUPZl{6~~b->&$&+7Ezu%d9F< zh|oNE-459`$Xo>w_$O+Qx+cQL{0=kVdm!&p mV ztSSxApOwA>M&zk *`fAh=4CKB>tpB|gui?Wuj z8vx1m?nEp)Dqr8L3-5=IgGFLXkK?po04#LOXVLEz|CrAR2Cy19sOz3}HX0uYEG~ct zE53yHyD|a|WFs=W39g{!6)edW=d*=$ri7aF?PBE cmP8(JX z$(=l{LZW7IpNHZgL3~-MN&a(lk-%^#O}Kv?->S1W|FJl+Y*V(LlGNDCT}9%XQ@RzJ zNv|Vger4+@!=|yK=T};|o>q_{#&UXmQN*xY#>jLeWyQ>{KRej>qIi2VsOe!<6K(5v zA1)J$l})(tlB*%MxZVLY$&GE8mHwPShF0Jt2EIN_o`ViAKCRN %X*-?UL-<=!}||d094N5#{9Bcq#O8AMK8f ziy(Y8J#JO_YE@8mximgbM^!UEpVzr**GJX#Vy2x4gO%~l{_J&*?_M4Ou>94Z-r2;< zS;VV(DvlsO%#B41L~Cs9wH88|Hl;3NK7o?&3<)5ueVgo*^<>24Z|Ue)e6wQ(Ov~Cw z?U>Fka^{{1eX_hJz)^mf*%cX CXoq9nR|PMmh_iDJ!**KT~xV^50Q+nE(F9d~=P zp$9OrKkPj{3rnqVRGlgzM-_ib_6I?k=o3$DUAn_d`jX~_sHf;v!R#jEf!Rp?+ICL| zwkb1s2rbe7s&nZkgv%Wz|CR8ox5t_Q0W~*0kEb2N8%zqbrTJuSYJ0>8RnY0A3>lf< zkizdLGqT4*-{U!Gx1`y}YrR9ge)%LF%Jdu7HM= Yb z?RsylWZNP3SXtKZvK*|r3?wtMCOh2m4Y#DkT}Ds{l~esiq?R7Jhi#Ei9BeDO+j7+4 z(7V{xKEQ$O->9(^wl_x9*He<&G*rP1`ceHSk`Z0of=fxPw}_eRJVVz2&&QSvTslq^ zcT7rnBh0e zYOJy4W5%c|ms}jN6`6j;GEA{8 zpLyV+*=qdC@VepC !GQD=ZA^zs`Vkb~5iMuLraO005K3Wh)7865rJ7pC n2hFjSc%XwJloAyas!xp%Pbf@&Wgdq zYo-Ni9S{4n%bZ^VRx3OoR4=P_5_Ov^pD~xrFw`Nd)pERdNOIOKX8x8ec3vxU($Dp; zl>DO8@4@88$}A>jPfy3KZ|ktGx-`9bnrGJkc`%fE@uZZHiY22)vN2;)U{#G!0jCU& zc3DBH5SY==yHM2t33G3I=Jndt)hE$gE|oZUJ2^>9^w@{^2y`8^{fI*%v~H-LY`r(p zj8lvEf;M*e6O4Q;H2NaDn%{Ewm#kC$+`HdZ@}CuVPYcZwC0TT`Ey&q{ZWHo{J)Hy7 z?}s|thqHuDD3uKUb{b1yo$5JU2U#jVE8$FEY5cxv;uD{`Nu!x2`NCu~)9E5u3SJ`B zC7$c|o3h>VH JAF~Mz(Xdft>L(y%@72ux+ zW?TNdWLDMpf2uYKv-xyCjaf6aJnvz8vuUfh_Ng2CJ1OU5Ru D3pmVY~Wu?vZg?+Dac88FPrO#m_s$v9sj5{=K zev`cPxWzjBL}bXS_WgsJT|~#{eX*~;q1Ehs@8@sxT&;VY$ >-f~M+D8eSd0X!KW>i3WqI(o;=99U_@EEy}$ z%+%ZE5%X6*MtDCwT?~&g3w9b&oIid-InM;+Jaj(KRH!F~M9KZIm-N7qNO?a>QaVh+ zIDrWlR+U=|Orpdl7=a7U=(8;J?&JYbABqK