Skip to content

Commit c772356

Browse files
authored
Add Auth (#2)
* feat: add home and auth ui * chore: use named volumes * feat: add auth routes * feat: integrate client+server auth
1 parent ee0718e commit c772356

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2352
-36
lines changed

.gitignore

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,3 @@ target
3838
*.njsproj
3939
*.sln
4040
*.sw?
41-
42-
# Volumes
43-
redis-data
44-

apps/client/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@
1212
"preview": "vite preview"
1313
},
1414
"dependencies": {
15+
"@tanstack/react-query": "^5.90.2",
1516
"react": "^19.1.1",
16-
"react-dom": "^19.1.1"
17+
"react-dom": "^19.1.1",
18+
"react-hot-toast": "^2.6.0",
19+
"react-icons": "^5.5.0",
20+
"react-router": "^7.9.3",
21+
"react-router-dom": "^7.9.3",
22+
"zod": "^4.1.12",
23+
"zustand": "^5.0.8"
1724
},
1825
"devDependencies": {
1926
"@types/react": "^19.1.10",

apps/client/src/App.tsx

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import React, { useState } from 'react'
2+
import { FaGithub, FaGoogle, FaEnvelope, FaLock, FaUser } from 'react-icons/fa'
3+
import { useNavigate } from 'react-router-dom'
4+
import toast, { Toaster } from 'react-hot-toast'
5+
6+
import Button from '../common/Button'
7+
import Input from '../common/Input'
8+
import { signUp, login } from '../../services/auth.service'
9+
import { signUpValidator, loginValidator } from '../../validators/auth.validators'
10+
import { useUserStore } from '../../store/users'
11+
import { useValidate } from '../../hooks/useValidate'
12+
13+
const AuthForm: React.FC = () => {
14+
const [isSignUp, setIsSignUp] = useState(false)
15+
const [formData, setFormData] = useState({
16+
name: '',
17+
email: '',
18+
password: '',
19+
})
20+
const [errors, setErrors] = useState<{ name?: string; email?: string; password?: string }>({})
21+
const [loading, setLoading] = useState(false)
22+
const navigate = useNavigate()
23+
const setUser = useUserStore(state => state.setUser)
24+
25+
const validator = isSignUp ? signUpValidator : loginValidator
26+
27+
const { validateField, validateForm } = useValidate(validator, setErrors, isSignUp, formData)
28+
29+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
30+
setFormData({ ...formData, [e.target.name]: e.target.value })
31+
validateField(e.target.name as 'name' | 'email' | 'password')
32+
}
33+
34+
const handleSubmit = async (e: React.FormEvent) => {
35+
e.preventDefault()
36+
if (!validateForm()) {
37+
toast.error('Please fix the errors!', { style: { background: '#171717', color: '#ff8800' } })
38+
return
39+
}
40+
setLoading(true)
41+
try {
42+
const res = isSignUp
43+
? await signUp({ name: formData.name, email: formData.email, password: formData.password })
44+
: await login({ email: formData.email, password: formData.password })
45+
46+
if (res.error) {
47+
const errorMessage =
48+
typeof res.error === 'string'
49+
? res.error
50+
: res.error.prettyMessage || res.error.message || 'An error occurred'
51+
toast.error(errorMessage, { style: { background: '#171717', color: '#ff8800' } })
52+
} else if (res.data?.token) {
53+
setUser({ id: 0, name: formData.name, email: formData.email })
54+
toast.success('Success!', { style: { background: '#171717', color: '#ff8800' } })
55+
navigate('/dash')
56+
}
57+
} catch (error) {
58+
console.error('Auth error:', error)
59+
toast.error('Something went wrong!', { style: { background: '#171717', color: '#ff8800' } })
60+
} finally {
61+
setLoading(false)
62+
}
63+
}
64+
65+
return (
66+
<div className="h-full bg-neutral-950 p-12 flex flex-col justify-center">
67+
<Toaster position="top-center" />
68+
<div className="max-w-md w-full mx-auto">
69+
<div className="mb-8">
70+
<h1 className="text-4xl font-bold text-white mb-2">
71+
{isSignUp ? 'Create Account' : 'Welcome Back'}
72+
</h1>
73+
<p className="text-gray-400 font-light">
74+
{isSignUp
75+
? 'Sign up to start transforming your meetings'
76+
: 'Sign in to continue to Verse'}
77+
</p>
78+
</div>
79+
80+
<div className="space-y-3 mb-8">
81+
<Button
82+
variant="secondary"
83+
className="w-full flex items-center justify-center gap-3 px-6 py-3"
84+
>
85+
<FaGithub size={20} />
86+
Continue with GitHub
87+
</Button>
88+
89+
<Button
90+
variant="secondary"
91+
className="w-full flex items-center justify-center gap-3 px-6 py-3"
92+
>
93+
<FaGoogle size={20} />
94+
Continue with Google
95+
</Button>
96+
</div>
97+
98+
<div className="relative mb-8">
99+
<div className="absolute inset-0 flex items-center">
100+
<div className="w-full border-t border-neutral-800"></div>
101+
</div>
102+
<div className="relative flex justify-center text-sm">
103+
<span className="px-4 bg-neutral-950 text-gray-400">or</span>
104+
</div>
105+
</div>
106+
107+
<form onSubmit={handleSubmit} className="space-y-4">
108+
{isSignUp && (
109+
<div className="relative">
110+
<FaUser className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" />
111+
<Input
112+
type="text"
113+
name="name"
114+
placeholder="Full Name"
115+
value={formData.name}
116+
onChange={handleChange}
117+
className="pl-12"
118+
required
119+
onBlur={() => validateField('name')}
120+
/>
121+
{errors.name && <p className="text-xs text-orange-500 mt-1">{errors.name}</p>}
122+
</div>
123+
)}
124+
125+
<div className="relative">
126+
<FaEnvelope className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" />
127+
<Input
128+
type="email"
129+
name="email"
130+
placeholder="Email Address"
131+
value={formData.email}
132+
onChange={handleChange}
133+
className="pl-12"
134+
required
135+
onBlur={() => validateField('email')}
136+
/>
137+
{errors.email && <p className="text-xs text-orange-500 mt-1">{errors.email}</p>}
138+
</div>
139+
140+
<div className="relative">
141+
<FaLock className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" />
142+
<Input
143+
type="password"
144+
name="password"
145+
placeholder="Password"
146+
value={formData.password}
147+
onChange={handleChange}
148+
className="pl-12"
149+
required
150+
onBlur={() => validateField('password')}
151+
/>
152+
{errors.password && <p className="text-xs text-orange-500 mt-1">{errors.password}</p>}
153+
</div>
154+
155+
{!isSignUp && (
156+
<div className="flex justify-end">
157+
<a
158+
href="#"
159+
className="text-sm text-secondary hover:text-orange-500 transition-colors"
160+
>
161+
Forgot password?
162+
</a>
163+
</div>
164+
)}
165+
166+
<Button type="submit" variant="primary" className="w-full" disabled={loading}>
167+
{loading ? 'Loading...' : isSignUp ? 'Sign Up' : 'Sign In'}
168+
</Button>
169+
</form>
170+
171+
<div className="mt-6 text-center">
172+
<p className="text-gray-400 font-light">
173+
{isSignUp ? 'Already have an account?' : "Don't have an account?"}{' '}
174+
<button
175+
type="button"
176+
onClick={() => setIsSignUp(!isSignUp)}
177+
className="text-secondary hover:text-orange-500 font-normal transition-colors"
178+
>
179+
{isSignUp ? 'Sign In' : 'Sign Up'}
180+
</button>
181+
</p>
182+
</div>
183+
184+
{isSignUp && (
185+
<p className="mt-6 text-center text-xs text-gray-500">
186+
By signing up, you agree to our{' '}
187+
<a href="#" className="text-secondary hover:text-orange-500">
188+
Terms of Service
189+
</a>{' '}
190+
and{' '}
191+
<a href="#" className="text-secondary hover:text-orange-500">
192+
Privacy Policy
193+
</a>
194+
</p>
195+
)}
196+
</div>
197+
</div>
198+
)
199+
}
200+
201+
export default AuthForm
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react'
2+
import { FaMicrophone, FaRobot, FaTasks } from 'react-icons/fa'
3+
4+
const InfoCard: React.FC = () => {
5+
return (
6+
<div className="h-full bg-gradient-to-br from-primary via-neutral-900 to-secondary/40 p-12 flex flex-col relative overflow-hidden">
7+
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-secondary/20 rounded-full blur-3xl"></div>
8+
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-secondary/10 rounded-full blur-3xl"></div>
9+
10+
<div className="relative z-10 h-full flex flex-col">
11+
<div className="mb-auto">
12+
<h2 className="text-5xl font-bold text-secondary">Verse</h2>
13+
<p className="text-gray-300 text-sm mt-2">AI-Powered Meeting Intelligence</p>
14+
</div>
15+
16+
<div className="flex-grow flex items-center">
17+
<div className="w-full">
18+
<h3 className="text-3xl font-bold text-white mb-8">Transform Your Meetings with AI</h3>
19+
20+
<div className="space-y-6">
21+
<div className="flex items-start gap-4">
22+
<div className="p-3 bg-white/10 backdrop-blur-sm rounded-xl flex-shrink-0 border border-white/20">
23+
<FaMicrophone className="text-secondary text-xl" />
24+
</div>
25+
<div>
26+
<h4 className="text-white font-semibold text-lg mb-1">Real-time Transcription</h4>
27+
<p className="text-gray-300">
28+
Get accurate, live transcriptions of every meeting
29+
</p>
30+
</div>
31+
</div>
32+
33+
<div className="flex items-start gap-4">
34+
<div className="p-3 bg-white/10 backdrop-blur-sm rounded-xl flex-shrink-0 border border-white/20">
35+
<FaRobot className="text-secondary text-xl" />
36+
</div>
37+
<div>
38+
<h4 className="text-white font-semibold text-lg mb-1">AI Summaries</h4>
39+
<p className="text-gray-300">
40+
Intelligent summaries with key insights and action items
41+
</p>
42+
</div>
43+
</div>
44+
45+
<div className="flex items-start gap-4">
46+
<div className="p-3 bg-white/10 backdrop-blur-sm rounded-xl flex-shrink-0 border border-white/20">
47+
<FaTasks className="text-secondary text-xl" />
48+
</div>
49+
<div>
50+
<h4 className="text-white font-semibold text-lg mb-1">Automatic Tasks</h4>
51+
<p className="text-gray-300">
52+
Seamlessly create and assign tasks from discussions
53+
</p>
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
</div>
59+
</div>
60+
</div>
61+
)
62+
}
63+
64+
export default InfoCard
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react'
2+
3+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4+
variant?: 'primary' | 'secondary' | 'outline'
5+
children: React.ReactNode
6+
}
7+
8+
const Button: React.FC<ButtonProps> = ({
9+
variant = 'primary',
10+
children,
11+
className = '',
12+
...props
13+
}) => {
14+
const baseStyles = 'px-8 py-4 rounded-full border-none font-semibold transition-all duration-300'
15+
16+
const variants = {
17+
primary: 'bg-secondary text-neutral-950 hover:bg-orange-500',
18+
secondary: 'bg-neutral-900 hover:bg-neutral-800 text-white',
19+
outline:
20+
'bg-transparent border-2 border-secondary text-secondary hover:bg-secondary hover:text-neutral-950',
21+
}
22+
23+
return (
24+
<button className={`${baseStyles} ${variants[variant]} ${className}`} {...props}>
25+
{children}
26+
</button>
27+
)
28+
}
29+
30+
export default Button

0 commit comments

Comments
 (0)