From 522549da5a8ff1174924a4f9171e04b821e2d386 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 12 Aug 2025 22:38:12 -0700 Subject: [PATCH 1/5] wip: adding log in --- server.sh => dock.sh | 0 frontend/src/components/Login/LoginReturn.tsx | 2 +- frontend/src/hooks/useUserStore.ts | 56 +++++++++++++++++-- frontend/src/lib/{ => api}/api.ts | 0 frontend/src/{ => lib}/api/authRoutes.ts | 2 +- 5 files changed, 53 insertions(+), 7 deletions(-) rename server.sh => dock.sh (100%) rename frontend/src/lib/{ => api}/api.ts (100%) rename frontend/src/{ => lib}/api/authRoutes.ts (98%) diff --git a/server.sh b/dock.sh similarity index 100% rename from server.sh rename to dock.sh diff --git a/frontend/src/components/Login/LoginReturn.tsx b/frontend/src/components/Login/LoginReturn.tsx index fb709ff..72701e3 100644 --- a/frontend/src/components/Login/LoginReturn.tsx +++ b/frontend/src/components/Login/LoginReturn.tsx @@ -1,5 +1,5 @@ export default function LoginReturn() { - return
LoginReturn
; + return
Welcome back
; } \ No newline at end of file diff --git a/frontend/src/hooks/useUserStore.ts b/frontend/src/hooks/useUserStore.ts index 943236f..95bed6f 100644 --- a/frontend/src/hooks/useUserStore.ts +++ b/frontend/src/hooks/useUserStore.ts @@ -1,12 +1,15 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; -import getAuthRoutes from '../api/authRoutes'; +import getAuthRoutes from '@/lib/api/authRoutes'; import { UserInfo } from '@/types/authTypes'; interface UserState extends UserInfo { accessExp: number; guestLogin: (displayName: string) => Promise; + passLogin: (email: string, password: string) => Promise; + getLoginCode: (email: string) => Promise; + codeLogin: (email: string, code: string) => Promise; logout: () => void; refresh: () => Promise; @@ -23,6 +26,13 @@ const blankUser = { const authRoutes = getAuthRoutes(); +const retError = (error: unknown): Error => { + if (error instanceof Error) { + return error; + } + return new Error(String(error)); +}; + const useUserStore = create()( persist( (set, get) => { @@ -40,10 +50,46 @@ const useUserStore = create()( return null; } catch (error) { console.warn('guestLogin err', error); - if (error instanceof Error) { - return error; - } - return new Error(String(error)); + return retError(error); + } + }, + passLogin: async (email: string, password: string) => { + try { + const { user, accessExp } = await authRoutes.loginWithPassword(email, password); + set({ + username: user.username, + displayName: user.displayName, + accountType: user.accountType, + accessExp, + }); + return null; + } catch (error) { + console.warn('passLogin err', error); + return retError(error); + } + }, + getLoginCode: async (email: string) => { + try { + await authRoutes.getLoginCode(email); + return null; + } catch (error) { + console.warn('getLoginCode err', error); + return retError(error); + } + }, + codeLogin: async (email: string, code: string) => { + try { + const { user, accessExp } = await authRoutes.loginWithCode(email, code); + set({ + username: user.username, + displayName: user.displayName, + accountType: user.accountType, + accessExp, + }); + return null; + } catch (error) { + console.warn('codeLogin err', error); + return retError(error); } }, logout: () => { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api/api.ts similarity index 100% rename from frontend/src/lib/api.ts rename to frontend/src/lib/api/api.ts diff --git a/frontend/src/api/authRoutes.ts b/frontend/src/lib/api/authRoutes.ts similarity index 98% rename from frontend/src/api/authRoutes.ts rename to frontend/src/lib/api/authRoutes.ts index 5e4a425..fcf764d 100644 --- a/frontend/src/api/authRoutes.ts +++ b/frontend/src/lib/api/authRoutes.ts @@ -1,4 +1,4 @@ -import api from '@/lib/api'; +import api from './api'; import { AuthResponse, GuestRequest, From 3cdd17afab86f7679bd58b63d73a89e6e348da0e Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 21 Aug 2025 22:30:50 -0700 Subject: [PATCH 2/5] feat: added returning user login --- frontend/package-lock.json | 213 ++++++++++++++++++ frontend/package.json | 1 + frontend/src/app/layout.tsx | 2 +- frontend/src/components/Login/CodeLogin.tsx | 163 ++++++++++++++ frontend/src/components/Login/LoginBox.tsx | 79 ++----- frontend/src/components/Login/LoginGuest.tsx | 2 +- frontend/src/components/Login/LoginReturn.tsx | 48 +++- .../src/components/Login/PasswordLogin.tsx | 95 ++++++++ .../src/components/{Chat => chat}/ChatBox.tsx | 0 .../components/{Chat => chat}/ChatWindow.tsx | 0 .../src/components/switchTab/Selector.tsx | 44 ++++ frontend/src/components/switchTab/Tabs.tsx | 60 +++++ 12 files changed, 643 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/Login/CodeLogin.tsx create mode 100644 frontend/src/components/Login/PasswordLogin.tsx rename frontend/src/components/{Chat => chat}/ChatBox.tsx (100%) rename frontend/src/components/{Chat => chat}/ChatWindow.tsx (100%) create mode 100644 frontend/src/components/switchTab/Selector.tsx create mode 100644 frontend/src/components/switchTab/Tabs.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3382c39..9826d96 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@headlessui/react": "^2.2.7", "@tsparticles/engine": "^3.8.1", "@tsparticles/react": "^3.0.0", "@tsparticles/slim": "^3.8.1", @@ -1982,6 +1983,79 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz", + "integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2715,6 +2789,103 @@ "node": ">=12.4.0" } }, + "node_modules/@react-aria/focus": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.0.tgz", + "integrity": "sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.4", + "@react-aria/utils": "^3.30.0", + "@react-types/shared": "^3.31.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.4", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.4.tgz", + "integrity": "sha512-HBQMxgUPHrW8V63u9uGgBymkMfj6vdWbB0GgUJY49K9mBKMsypcHeWkWM6+bF7kxRO728/IK8bWDV6whDbqjHg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.30.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.31.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.0.tgz", + "integrity": "sha512-ydA6y5G1+gbem3Va2nczj/0G0W7/jUVo/cbN10WA5IizzWIwMP5qhFr7macgbKfHMkZ+YZC3oXnt2NNre5odKw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.31.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.31.0.tgz", + "integrity": "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3291,6 +3462,33 @@ "tailwindcss": "4.1.8" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -8927,6 +9125,12 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -9320,6 +9524,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index f197445..59af000 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "lint": "next lint" }, "dependencies": { + "@headlessui/react": "^2.2.7", "@tsparticles/engine": "^3.8.1", "@tsparticles/react": "^3.0.0", "@tsparticles/slim": "^3.8.1", diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 522ade1..5b3cd03 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -3,7 +3,7 @@ import { Roboto, Poppins } from 'next/font/google'; import './globals.css'; import NavBar from '@/components/UI/NavBar'; import Footer from '@/components/UI/Footer'; -import ChatWindow from '@/components/Chat/ChatWindow'; +import ChatWindow from '@/components/chat/ChatWindow'; import ClientInit from '@/components/ClientInit'; import CookieConsent from '@/components/UI/CookieConsent'; diff --git a/frontend/src/components/Login/CodeLogin.tsx b/frontend/src/components/Login/CodeLogin.tsx new file mode 100644 index 0000000..2d96a17 --- /dev/null +++ b/frontend/src/components/Login/CodeLogin.tsx @@ -0,0 +1,163 @@ +import { useState } from 'react'; +import { ArrowLeft, Mail } from 'lucide-react'; +import useUserStore from '@/hooks/useUserStore'; +import Button from '../UI/Button'; +import Input from '../UI/Input'; + +interface CodeLoginProps { + onBack: () => void; +} + +export default function CodeLogin({ onBack }: CodeLoginProps) { + const [email, setEmail] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isCodeSent, setIsCodeSent] = useState(false); + const [code, setCode] = useState(''); + const [error, setError] = useState(''); + const userStore = useUserStore(); + + const handleSendCode = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + if (!email) { + setError('Please enter your email'); + return; + } + + setIsLoading(true); + try { + const err = await userStore.getLoginCode(email); + if (err) { + setError(err.message || 'Failed to send code. Please try again.'); + return; + } + setIsCodeSent(true); + } catch (err) { + setError('An unexpected error occurred. Please try again.'); + console.error('Send code error:', err); + } finally { + setIsLoading(false); + } + }; + + const handleVerifyCode = async (e: React.FormEvent) => { + e.preventDefault(); + if (!code) { + setError('Please enter the verification code'); + return; + } + + setIsLoading(true); + setError(''); + + try { + const err = await userStore.codeLogin(email, code); + if (err) { + setError(err.message || 'Invalid verification code. Please try again.'); + } + // On success, the user will be redirected by the auth flow + } catch (err) { + setError('An error occurred during verification. Please try again.'); + console.error('Verification error:', err); + } finally { + setIsLoading(false); + } + }; + + if (!isCodeSent) { + return ( +
+
+
+ + setEmail(e.target.value)} + placeholder="you@example.com" + /> +
+ +
+ +
+ + {error && ( +
{error}
+ )} +
+ +
+ +
+
+ ); + } + + return ( +
+
+
+

+ We've sent a 6-digit code to {email} +

+ + setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + placeholder="123456" + className="text-center tracking-widest font-mono" + /> +
+ +
+ +
+ + {error && ( +
{error}
+ )} +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Login/LoginBox.tsx b/frontend/src/components/Login/LoginBox.tsx index 07b651a..668968b 100644 --- a/frontend/src/components/Login/LoginBox.tsx +++ b/frontend/src/components/Login/LoginBox.tsx @@ -1,12 +1,14 @@ import { useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { Selector } from '../switchTab/Selector'; +import { Tabs } from '../switchTab/Tabs'; import LoginGuest from './LoginGuest'; import LoginReturn from './LoginReturn'; -type TabType = 'new' | 'returning'; - export default function LoginBox() { - const [activeTab, setActiveTab] = useState('new'); + const [activeTab, setActiveTab] = useState(0); + + const tabLabels = ['New here?', 'Returning?']; + const tabComponents = [, ]; return (
@@ -14,63 +16,22 @@ export default function LoginBox() {

Some live features like chat requires an account.

-
- - + +
+
- - - {activeTab === 'new' ? : } - - +
+ +
); } diff --git a/frontend/src/components/Login/LoginGuest.tsx b/frontend/src/components/Login/LoginGuest.tsx index 7480f07..8f02d79 100644 --- a/frontend/src/components/Login/LoginGuest.tsx +++ b/frontend/src/components/Login/LoginGuest.tsx @@ -29,7 +29,7 @@ export default function LoginGuest() { return (
-
; -} \ No newline at end of file + const [activeTab, setActiveTab] = useState(0); + + const handleBack = () => setActiveTab(0); + + const MethodSelect = () => ( +
+

How would you like to log in?

+
+ + +
+
+ ); + + const tabs = [ + , + , + , + ]; + + return ( +
+

Welcome Back!

+ + +
+ ); +} diff --git a/frontend/src/components/Login/PasswordLogin.tsx b/frontend/src/components/Login/PasswordLogin.tsx new file mode 100644 index 0000000..929e604 --- /dev/null +++ b/frontend/src/components/Login/PasswordLogin.tsx @@ -0,0 +1,95 @@ +import { useState } from 'react'; +import { ArrowLeft } from 'lucide-react'; +import useUserStore from '@/hooks/useUserStore'; +import Button from '../UI/Button'; +import Input from '../UI/Input'; + +interface PasswordLoginProps { + onBack: () => void; +} + +export default function PasswordLogin({ onBack }: PasswordLoginProps) { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const userStore = useUserStore(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + const err = await userStore.passLogin(email, password); + if (err) { + setError(err.message || 'Failed to log in. Please check your credentials.'); + } + } catch (err) { + setError('An unexpected error occurred. Please try again.'); + console.error('Login error:', err); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ +
+ + setEmail(e.target.value)} + placeholder="you@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + /> +
+ +
+ +
+ + {error && ( +
{error}
+ )} + + +
+ +
+
+ ); +} diff --git a/frontend/src/components/Chat/ChatBox.tsx b/frontend/src/components/chat/ChatBox.tsx similarity index 100% rename from frontend/src/components/Chat/ChatBox.tsx rename to frontend/src/components/chat/ChatBox.tsx diff --git a/frontend/src/components/Chat/ChatWindow.tsx b/frontend/src/components/chat/ChatWindow.tsx similarity index 100% rename from frontend/src/components/Chat/ChatWindow.tsx rename to frontend/src/components/chat/ChatWindow.tsx diff --git a/frontend/src/components/switchTab/Selector.tsx b/frontend/src/components/switchTab/Selector.tsx new file mode 100644 index 0000000..1343504 --- /dev/null +++ b/frontend/src/components/switchTab/Selector.tsx @@ -0,0 +1,44 @@ +import { cn } from '@/lib/utils'; +import { motion } from 'framer-motion'; + +interface SelectorProps { + tabs: readonly string[]; + activeTab: number; + onTabChange: (index: number) => void; + className?: string; +} + +export function Selector({ + tabs, + activeTab, + onTabChange, + className = '', +}: SelectorProps) { + return ( +
+ {tabs.map((tab, index) => ( + + ))} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/switchTab/Tabs.tsx b/frontend/src/components/switchTab/Tabs.tsx new file mode 100644 index 0000000..2dd119f --- /dev/null +++ b/frontend/src/components/switchTab/Tabs.tsx @@ -0,0 +1,60 @@ +import { motion, AnimatePresence } from 'framer-motion'; +import { useEffect, useRef } from 'react'; + +interface TabsProps { + tabs: readonly React.ReactNode[]; + activeTab: number; + className?: string; +} + +export function Tabs({ + tabs, + activeTab, + className = '', +}: TabsProps) { + const prevTabRef = useRef(activeTab); + + useEffect(() => { + prevTabRef.current = activeTab; + }, [activeTab]); + + const prevTab = prevTabRef.current; + const direction = activeTab > prevTab ? 'right' : 'left'; + + const variants = { + enter: (dir: 'left' | 'right') => ({ + x: dir === 'right' ? 50 : -50, + opacity: 0, + }), + center: { + x: 0, + opacity: 1, + }, + exit: (dir: 'left' | 'right') => ({ + x: dir === 'right' ? -50 : 50, + opacity: 0, + }), + }; + + return ( +
+ + + {tabs[activeTab]} + + +
+ ); +} \ No newline at end of file From fd686762452a5fdce293e5015973e1dd50759eae Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 8 Nov 2025 19:31:45 -0800 Subject: [PATCH 3/5] feat: login boxes --- frontend/src/components/chat/ChatWindow.tsx | 7 +++--- .../chat/{ => loggedin}/ChatBox.tsx | 4 ++-- .../components/chat/loggedin/LoggedinBox.tsx | 11 +++++++++ .../{Login => chat/login}/CodeLogin.tsx | 4 ++-- .../{Login => chat/login}/LoginBox.tsx | 24 ++++++++----------- .../{Login => chat/login}/LoginGuest.tsx | 4 ++-- .../{Login => chat/login}/LoginReturn.tsx | 4 ++-- .../{Login => chat/login}/PasswordLogin.tsx | 4 ++-- .../src/components/switchTab/Selector.tsx | 2 +- frontend/src/components/switchTab/Tabs.tsx | 2 +- 10 files changed, 37 insertions(+), 29 deletions(-) rename frontend/src/components/chat/{ => loggedin}/ChatBox.tsx (98%) create mode 100644 frontend/src/components/chat/loggedin/LoggedinBox.tsx rename frontend/src/components/{Login => chat/login}/CodeLogin.tsx (98%) rename frontend/src/components/{Login => chat/login}/LoginBox.tsx (55%) rename frontend/src/components/{Login => chat/login}/LoginGuest.tsx (95%) rename frontend/src/components/{Login => chat/login}/LoginReturn.tsx (93%) rename frontend/src/components/{Login => chat/login}/PasswordLogin.tsx (97%) diff --git a/frontend/src/components/chat/ChatWindow.tsx b/frontend/src/components/chat/ChatWindow.tsx index 5cf8f64..fce2c93 100644 --- a/frontend/src/components/chat/ChatWindow.tsx +++ b/frontend/src/components/chat/ChatWindow.tsx @@ -3,10 +3,11 @@ import { useState } from 'react'; import useUserStore from '@/hooks/useUserStore'; import { useWSConnect } from '@/hooks/useWebsocket'; -import LoginBox from '../Login/LoginBox'; -import ChatBox from './ChatBox'; +import LoginBox from './login/LoginBox'; +import ChatBox from './loggedin/ChatBox'; import Button from '../UI/Button'; import { MessageCircleMore, X } from 'lucide-react'; +import LoggedinBox from './loggedin/LoggedinBox'; export default function ChatWindow() { const [isOpen, setIsOpen] = useState(false); @@ -42,7 +43,7 @@ export default function ChatWindow() {
- {loggedIn ? : } + {loggedIn ? : }
) : ( diff --git a/frontend/src/components/chat/ChatBox.tsx b/frontend/src/components/chat/loggedin/ChatBox.tsx similarity index 98% rename from frontend/src/components/chat/ChatBox.tsx rename to frontend/src/components/chat/loggedin/ChatBox.tsx index 9349e66..c959ade 100644 --- a/frontend/src/components/chat/ChatBox.tsx +++ b/frontend/src/components/chat/loggedin/ChatBox.tsx @@ -1,8 +1,8 @@ import { useState, useEffect, useRef } from 'react'; import useWebSocket from '@/hooks/useWebsocket'; import useUserStore from '@/hooks/useUserStore'; -import Input from '../UI/Input'; -import Button from '../UI/Button'; +import Input from '../../UI/Input'; +import Button from '../../UI/Button'; import { msgError } from '@/types/wsTypes'; import { SquarePen, LogOut, DoorOpen } from 'lucide-react'; diff --git a/frontend/src/components/chat/loggedin/LoggedinBox.tsx b/frontend/src/components/chat/loggedin/LoggedinBox.tsx new file mode 100644 index 0000000..472673c --- /dev/null +++ b/frontend/src/components/chat/loggedin/LoggedinBox.tsx @@ -0,0 +1,11 @@ +import { useState } from 'react'; +import Tabs from '@/components/switchTab/Tabs'; + +export default function LoggedinBox() { + const [active, setActive] = useState(1); + const tabs = [ + + ]; + + return
LoggedinBox
; +} diff --git a/frontend/src/components/Login/CodeLogin.tsx b/frontend/src/components/chat/login/CodeLogin.tsx similarity index 98% rename from frontend/src/components/Login/CodeLogin.tsx rename to frontend/src/components/chat/login/CodeLogin.tsx index 2d96a17..dbf2c5f 100644 --- a/frontend/src/components/Login/CodeLogin.tsx +++ b/frontend/src/components/chat/login/CodeLogin.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; import { ArrowLeft, Mail } from 'lucide-react'; import useUserStore from '@/hooks/useUserStore'; -import Button from '../UI/Button'; -import Input from '../UI/Input'; +import Button from '../../UI/Button'; +import Input from '../../UI/Input'; interface CodeLoginProps { onBack: () => void; diff --git a/frontend/src/components/Login/LoginBox.tsx b/frontend/src/components/chat/login/LoginBox.tsx similarity index 55% rename from frontend/src/components/Login/LoginBox.tsx rename to frontend/src/components/chat/login/LoginBox.tsx index 668968b..c9e512f 100644 --- a/frontend/src/components/Login/LoginBox.tsx +++ b/frontend/src/components/chat/login/LoginBox.tsx @@ -1,14 +1,13 @@ import { useState } from 'react'; -import { Selector } from '../switchTab/Selector'; -import { Tabs } from '../switchTab/Tabs'; +import Selector from '../../switchTab/Selector'; +import Tabs from '../../switchTab/Tabs'; import LoginGuest from './LoginGuest'; import LoginReturn from './LoginReturn'; export default function LoginBox() { - const [activeTab, setActiveTab] = useState(0); - - const tabLabels = ['New here?', 'Returning?']; - const tabComponents = [, ]; + const [active, setActive] = useState(0); + const names = ['New here?', 'Returning?']; + const tabs = [, ]; return (
@@ -16,21 +15,18 @@ export default function LoginBox() {

Some live features like chat requires an account.

- +
- +
); diff --git a/frontend/src/components/Login/LoginGuest.tsx b/frontend/src/components/chat/login/LoginGuest.tsx similarity index 95% rename from frontend/src/components/Login/LoginGuest.tsx rename to frontend/src/components/chat/login/LoginGuest.tsx index 8f02d79..65ad92f 100644 --- a/frontend/src/components/Login/LoginGuest.tsx +++ b/frontend/src/components/chat/login/LoginGuest.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import Button from '../UI/Button'; -import Input from '../UI/Input'; +import Button from '../../UI/Button'; +import Input from '../../UI/Input'; import { Loader2 } from 'lucide-react'; import useUserStore from '@/hooks/useUserStore'; diff --git a/frontend/src/components/Login/LoginReturn.tsx b/frontend/src/components/chat/login/LoginReturn.tsx similarity index 93% rename from frontend/src/components/Login/LoginReturn.tsx rename to frontend/src/components/chat/login/LoginReturn.tsx index e5dd115..16a1adc 100644 --- a/frontend/src/components/Login/LoginReturn.tsx +++ b/frontend/src/components/chat/login/LoginReturn.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { ArrowLeft, X } from 'lucide-react'; -import { Tabs } from '../switchTab/Tabs'; +import { Tabs } from '../../switchTab/Tabs'; import PasswordLogin from './PasswordLogin'; import CodeLogin from './CodeLogin'; -import Button from '../UI/Button'; +import Button from '../../UI/Button'; export default function LoginReturn() { const [activeTab, setActiveTab] = useState(0); diff --git a/frontend/src/components/Login/PasswordLogin.tsx b/frontend/src/components/chat/login/PasswordLogin.tsx similarity index 97% rename from frontend/src/components/Login/PasswordLogin.tsx rename to frontend/src/components/chat/login/PasswordLogin.tsx index 929e604..2cc8f4a 100644 --- a/frontend/src/components/Login/PasswordLogin.tsx +++ b/frontend/src/components/chat/login/PasswordLogin.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; import { ArrowLeft } from 'lucide-react'; import useUserStore from '@/hooks/useUserStore'; -import Button from '../UI/Button'; -import Input from '../UI/Input'; +import Button from '../../UI/Button'; +import Input from '../../UI/Input'; interface PasswordLoginProps { onBack: () => void; diff --git a/frontend/src/components/switchTab/Selector.tsx b/frontend/src/components/switchTab/Selector.tsx index 1343504..8099623 100644 --- a/frontend/src/components/switchTab/Selector.tsx +++ b/frontend/src/components/switchTab/Selector.tsx @@ -8,7 +8,7 @@ interface SelectorProps { className?: string; } -export function Selector({ +export default function Selector({ tabs, activeTab, onTabChange, diff --git a/frontend/src/components/switchTab/Tabs.tsx b/frontend/src/components/switchTab/Tabs.tsx index 2dd119f..a6c5a75 100644 --- a/frontend/src/components/switchTab/Tabs.tsx +++ b/frontend/src/components/switchTab/Tabs.tsx @@ -7,7 +7,7 @@ interface TabsProps { className?: string; } -export function Tabs({ +export default function Tabs({ tabs, activeTab, className = '', From 2d3e7247108c6e037d887696bf11ba4ff81d90ab Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 8 Nov 2025 19:31:54 -0800 Subject: [PATCH 4/5] chore: switch to makefile --- Makefile | 40 ++++++++++++++++++++++++++++++++++++++++ dock.sh | 38 -------------------------------------- 2 files changed, 40 insertions(+), 38 deletions(-) create mode 100644 Makefile delete mode 100755 dock.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8183296 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +.PHONY: help up build rebuild down clear fsh bsh flog blog pr + +# Default target +.DEFAULT_GOAL := help + +# Tools +DC := docker compose + +help: ## Show this help message + @awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z0-9_.-]+:.*## / {printf "\033[36m%-10s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +up: ## Start all services + $(DC) up -d + +build: ## Build and start + $(DC) up --build -d + +rebuild: ## Rebuild and force recreate, then start + $(DC) up --build --force-recreate -d + +down: ## Stop containers + $(DC) down + +clear: ## Stop and clear local images + $(DC) down --volumes --rmi local + +fsh: ## Frontend shell + $(DC) exec next sh -i + +bsh: ## Backend shell + $(DC) exec go sh -i + +flog: ## Frontend logs + $(DC) logs next -f + +blog: ## Backend logs + $(DC) logs go -f + +pr: ## Run pull_request gh action locally + ./bin/act pull_request diff --git a/dock.sh b/dock.sh deleted file mode 100755 index 372ab8b..0000000 --- a/dock.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh - -case "$1" in - up) - docker compose up -d - ;; - build) - docker compose up --build -d - ;; - rebuild) - docker compose up --build --force-recreate -d - ;; - down) - docker compose down - ;; - clear) - docker compose down --volumes --rmi local - ;; - fsh) - docker compose exec next sh -i - ;; - bsh) - docker compose exec go sh -i - ;; - flog) - docker compose logs next -f - ;; - blog) - docker compose logs go -f - ;; - pr) - ./bin/act pull_request - ;; - *) - echo "Usage: $0 {up|build|down|clear|fsh|bsh|flog|blog}" - exit 1 - ;; -esac From 790d5e51f6e1e44bd4d7902450dfde81ec411039 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 9 Nov 2025 17:51:14 -0800 Subject: [PATCH 5/5] chore: fixed linting issues --- .../components/UI/particles/ParticleBanner.tsx | 2 +- frontend/src/components/chat/ChatWindow.tsx | 4 ++-- .../src/components/chat/loggedin/LoggedinBox.tsx | 7 ------- frontend/src/components/chat/login/CodeLogin.tsx | 16 +++++++++------- .../src/components/chat/login/LoginReturn.tsx | 5 ++--- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/UI/particles/ParticleBanner.tsx b/frontend/src/components/UI/particles/ParticleBanner.tsx index 2ead692..4d6f9d1 100644 --- a/frontend/src/components/UI/particles/ParticleBanner.tsx +++ b/frontend/src/components/UI/particles/ParticleBanner.tsx @@ -133,7 +133,7 @@ export default function ParticleBanner({ cancelAnimationFrame(animationFrameId); } }; - }, [dbSize]); + }, [dbSize, sampleSnapshot]); return (
diff --git a/frontend/src/components/chat/ChatWindow.tsx b/frontend/src/components/chat/ChatWindow.tsx index fce2c93..c0a18d2 100644 --- a/frontend/src/components/chat/ChatWindow.tsx +++ b/frontend/src/components/chat/ChatWindow.tsx @@ -7,7 +7,7 @@ import LoginBox from './login/LoginBox'; import ChatBox from './loggedin/ChatBox'; import Button from '../UI/Button'; import { MessageCircleMore, X } from 'lucide-react'; -import LoggedinBox from './loggedin/LoggedinBox'; +// import LoggedinBox from './loggedin/LoggedinBox'; export default function ChatWindow() { const [isOpen, setIsOpen] = useState(false); @@ -43,7 +43,7 @@ export default function ChatWindow() {
- {loggedIn ? : } + {loggedIn ? : }
) : ( diff --git a/frontend/src/components/chat/loggedin/LoggedinBox.tsx b/frontend/src/components/chat/loggedin/LoggedinBox.tsx index 472673c..dfb75fd 100644 --- a/frontend/src/components/chat/loggedin/LoggedinBox.tsx +++ b/frontend/src/components/chat/loggedin/LoggedinBox.tsx @@ -1,11 +1,4 @@ -import { useState } from 'react'; -import Tabs from '@/components/switchTab/Tabs'; - export default function LoggedinBox() { - const [active, setActive] = useState(1); - const tabs = [ - - ]; return
LoggedinBox
; } diff --git a/frontend/src/components/chat/login/CodeLogin.tsx b/frontend/src/components/chat/login/CodeLogin.tsx index dbf2c5f..01401ad 100644 --- a/frontend/src/components/chat/login/CodeLogin.tsx +++ b/frontend/src/components/chat/login/CodeLogin.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { ArrowLeft, Mail } from 'lucide-react'; +import { ArrowLeft } from 'lucide-react'; import useUserStore from '@/hooks/useUserStore'; import Button from '../../UI/Button'; import Input from '../../UI/Input'; @@ -23,7 +23,7 @@ export default function CodeLogin({ onBack }: CodeLoginProps) { setError('Please enter your email'); return; } - + setIsLoading(true); try { const err = await userStore.getLoginCode(email); @@ -46,10 +46,10 @@ export default function CodeLogin({ onBack }: CodeLoginProps) { setError('Please enter the verification code'); return; } - + setIsLoading(true); setError(''); - + try { const err = await userStore.codeLogin(email, code); if (err) { @@ -116,7 +116,7 @@ export default function CodeLogin({ onBack }: CodeLoginProps) {

- We've sent a 6-digit code to {email} + We've sent a 6-digit code to {email}

); -} \ No newline at end of file +} diff --git a/frontend/src/components/chat/login/LoginReturn.tsx b/frontend/src/components/chat/login/LoginReturn.tsx index 16a1adc..20bd6e9 100644 --- a/frontend/src/components/chat/login/LoginReturn.tsx +++ b/frontend/src/components/chat/login/LoginReturn.tsx @@ -1,9 +1,8 @@ import { useState } from 'react'; -import { ArrowLeft, X } from 'lucide-react'; -import { Tabs } from '../../switchTab/Tabs'; +import Tabs from '@/components/switchTab/Tabs'; import PasswordLogin from './PasswordLogin'; import CodeLogin from './CodeLogin'; -import Button from '../../UI/Button'; +import Button from '@/components/UI/Button'; export default function LoginReturn() { const [activeTab, setActiveTab] = useState(0);