Skip to content

Commit 84a59ab

Browse files
snomiaoclaudeCopilot
authored
feat: Add admin functionality to set nodes as unclaimed (#220)
* feat: Add admin functionality to set nodes as unclaimed This change adds a new "Unclaim" button to the admin node management page that allows administrators to remove the publisher association from a node. This enables scenarios where node authors need to transfer ownership or reclaim nodes under different publishers. Key changes: - Added useAdminUpdateNode hook to leverage admin privileges - Added "Unclaim" button with warning color in the actions column - Implemented confirmation modal with node and publisher details - Added handleUnclaim function that sets publisher to undefined - Only shows button for nodes that have a publisher 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * format: Apply prettier --fix changes * format: Apply prettier --fix changes * Update locales/es/common.json Co-authored-by: Copilot <[email protected]> * Update locales/fr/common.json Co-authored-by: Copilot <[email protected]> * Update locales/es/common.json Co-authored-by: Copilot <[email protected]> * format: Apply prettier --fix changes * feat: Add email/password login to authentication page Added comprehensive email/password authentication functionality to the /auth/login page alongside existing social login options. Changes: - Enhanced AuthUI component with email/password form toggle - Implemented Firebase email/password authentication with error handling - Added form validation and user feedback - Integrated with existing authentication flow and redirect logic - Added i18n support for all new UI text and error messages - Responsive design that works with existing UI theme The login page now offers users choice between: - Google OAuth (existing) - GitHub OAuth (existing) - Email/Password (new) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * format: Apply prettier --fix changes * format: Apply biome formatting from main branch 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 46bab2b commit 84a59ab

File tree

12 files changed

+549
-96
lines changed

12 files changed

+549
-96
lines changed

components/AuthUI/AuthUI.tsx

Lines changed: 199 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { getAuth } from 'firebase/auth'
2-
import { Button, Card } from 'flowbite-react'
1+
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'
2+
import { Button, Card, Label, TextInput } from 'flowbite-react'
33
import Image from 'next/image'
44
import Link from 'next/link'
55
import { useRouter } from 'next/router'
6-
import React from 'react'
6+
import React, { useState } from 'react'
77
import {
88
useSignInWithGithub,
99
useSignInWithGoogle,
@@ -21,6 +21,11 @@ const AuthUI: React.FC<{}> = ({}) => {
2121
const { t } = useNextTranslation()
2222
const router = useRouter()
2323
const auth = getAuth(app)
24+
// Email/password form state
25+
const [email, setEmail] = useState('')
26+
const [password, setPassword] = useState('')
27+
const [emailSignInLoading, setEmailSignInLoading] = useState(false)
28+
const [showEmailForm, setShowEmailForm] = useState(false)
2429
const [
2530
signInWithGoogle,
2631
_googleUser,
@@ -42,6 +47,50 @@ const AuthUI: React.FC<{}> = ({}) => {
4247
if (loggedIn) router.push(fromUrl ?? '/nodes')
4348
}, [loggedIn, router, fromUrl])
4449

50+
// Email/password sign-in handler
51+
const handleEmailSignIn = async (e: React.FormEvent) => {
52+
e.preventDefault()
53+
54+
// Basic validation
55+
if (!email) {
56+
toast.error(t('Email is required'))
57+
return
58+
}
59+
if (!password) {
60+
toast.error(t('Password is required'))
61+
return
62+
}
63+
if (password.length < 6) {
64+
toast.error(t('Password must be at least 6 characters'))
65+
return
66+
}
67+
68+
setEmailSignInLoading(true)
69+
try {
70+
await signInWithEmailAndPassword(auth, email, password)
71+
analytic.track('Sign In', { provider: 'Email' })
72+
toast.success(t('Sign In successful'))
73+
} catch (error: any) {
74+
console.error('Email sign-in error:', error)
75+
if (error.code === 'auth/user-not-found') {
76+
toast.error(t('No account found with this email'))
77+
} else if (error.code === 'auth/wrong-password') {
78+
toast.error(t('Incorrect password'))
79+
} else if (error.code === 'auth/invalid-email') {
80+
toast.error(t('Invalid email format'))
81+
} else if (error.code === 'auth/too-many-requests') {
82+
toast.error(t('Too many failed attempts. Please try again later'))
83+
} else {
84+
toast.error(
85+
error.message ||
86+
t('An unexpected error occurred. Please try again later.')
87+
)
88+
}
89+
} finally {
90+
setEmailSignInLoading(false)
91+
}
92+
}
93+
4594
// handle errors
4695
React.useEffect(() => {
4796
if (googleSignInError) {
@@ -92,86 +141,156 @@ const AuthUI: React.FC<{}> = ({}) => {
92141
{t('Sign In')}
93142
</h1>
94143

95-
<div className="mt-10 space-y-3 sm:space-x-4 sm:space-y-0">
96-
<Button
97-
color="gray"
98-
href="#"
99-
className="font-bold "
100-
onClick={() => {
101-
analytic.track('Sign In', {
102-
provider: 'Google',
103-
})
104-
signInWithGoogle()
105-
}}
106-
>
107-
<svg
108-
className="w-5 h-5 mr-2"
109-
viewBox="0 0 21 20"
110-
fill="#ffff"
111-
xmlns="http://www.w3.org/2000/svg"
144+
{!showEmailForm ? (
145+
<>
146+
<div className="mt-10 space-y-3 sm:space-x-4 sm:space-y-0">
147+
<Button
148+
color="gray"
149+
href="#"
150+
className="font-bold "
151+
onClick={() => {
152+
analytic.track('Sign In', {
153+
provider: 'Google',
154+
})
155+
signInWithGoogle()
156+
}}
157+
>
158+
<svg
159+
className="w-5 h-5 mr-2"
160+
viewBox="0 0 21 20"
161+
fill="#ffff"
162+
xmlns="http://www.w3.org/2000/svg"
163+
>
164+
<g clipPath="url(#clip0_13183_10121)">
165+
<path
166+
d="M20.3081 10.2303C20.3081 9.55056 20.253 8.86711 20.1354 8.19836H10.7031V12.0492H16.1046C15.8804 13.2911 15.1602 14.3898 14.1057 15.0879V17.5866H17.3282C19.2205 15.8449 20.3081 13.2728 20.3081 10.2303Z"
167+
fill="#3F83F8"
168+
/>
169+
<path
170+
d="M10.7019 20.0006C13.3989 20.0006 15.6734 19.1151 17.3306 17.5865L14.1081 15.0879C13.2115 15.6979 12.0541 16.0433 10.7056 16.0433C8.09669 16.0433 5.88468 14.2832 5.091 11.9169H1.76562V14.4927C3.46322 17.8695 6.92087 20.0006 10.7019 20.0006V20.0006Z"
171+
fill="#34A853"
172+
/>
173+
<path
174+
d="M5.08857 11.9169C4.66969 10.6749 4.66969 9.33008 5.08857 8.08811V5.51233H1.76688C0.348541 8.33798 0.348541 11.667 1.76688 14.4927L5.08857 11.9169V11.9169Z"
175+
fill="#FBBC04"
176+
/>
177+
<path
178+
d="M10.7019 3.95805C12.1276 3.936 13.5055 4.47247 14.538 5.45722L17.393 2.60218C15.5852 0.904587 13.1858 -0.0287217 10.7019 0.000673888C6.92087 0.000673888 3.46322 2.13185 1.76562 5.51234L5.08732 8.08813C5.87733 5.71811 8.09302 3.95805 10.7019 3.95805V3.95805Z"
179+
fill="#EA4335"
180+
/>
181+
</g>
182+
<defs>
183+
<clipPath id="clip0_13183_10121">
184+
<rect
185+
width="20"
186+
height="20"
187+
fill="white"
188+
transform="translate(0.5)"
189+
/>
190+
</clipPath>
191+
</defs>
192+
</svg>
193+
<span className="text-gray-900">
194+
{t('Continue with Google')}
195+
</span>
196+
</Button>
197+
</div>
198+
<Button
199+
color="gray"
200+
href="#"
201+
className="mt-2 font-bold hover:bg-gray-50"
202+
onClick={() => {
203+
analytic.track('Sign In', {
204+
provider: 'Github',
205+
})
206+
signInWithGithub(['user:email'])
207+
}}
112208
>
113-
<g clipPath="url(#clip0_13183_10121)">
114-
<path
115-
d="M20.3081 10.2303C20.3081 9.55056 20.253 8.86711 20.1354 8.19836H10.7031V12.0492H16.1046C15.8804 13.2911 15.1602 14.3898 14.1057 15.0879V17.5866H17.3282C19.2205 15.8449 20.3081 13.2728 20.3081 10.2303Z"
116-
fill="#3F83F8"
117-
/>
118-
<path
119-
d="M10.7019 20.0006C13.3989 20.0006 15.6734 19.1151 17.3306 17.5865L14.1081 15.0879C13.2115 15.6979 12.0541 16.0433 10.7056 16.0433C8.09669 16.0433 5.88468 14.2832 5.091 11.9169H1.76562V14.4927C3.46322 17.8695 6.92087 20.0006 10.7019 20.0006V20.0006Z"
120-
fill="#34A853"
121-
/>
122-
<path
123-
d="M5.08857 11.9169C4.66969 10.6749 4.66969 9.33008 5.08857 8.08811V5.51233H1.76688C0.348541 8.33798 0.348541 11.667 1.76688 14.4927L5.08857 11.9169V11.9169Z"
124-
fill="#FBBC04"
125-
/>
209+
<svg
210+
className="w-6 h-6 text-gray-800 dark:text-white"
211+
aria-hidden="true"
212+
xmlns="http://www.w3.org/2000/svg"
213+
width="24"
214+
height="24"
215+
fill="currentColor"
216+
viewBox="0 0 24 24"
217+
>
126218
<path
127-
d="M10.7019 3.95805C12.1276 3.936 13.5055 4.47247 14.538 5.45722L17.393 2.60218C15.5852 0.904587 13.1858 -0.0287217 10.7019 0.000673888C6.92087 0.000673888 3.46322 2.13185 1.76562 5.51234L5.08732 8.08813C5.87733 5.71811 8.09302 3.95805 10.7019 3.95805V3.95805Z"
128-
fill="#EA4335"
219+
fillRule="evenodd"
220+
d="M12.006 2a9.847 9.847 0 0 0-6.484 2.44 10.32 10.32 0 0 0-3.393 6.17 10.48 10.48 0 0 0 1.317 6.955 10.045 10.045 0 0 0 5.4 4.418c.504.095.683-.223.683-.494 0-.245-.01-1.052-.014-1.908-2.78.62-3.366-1.21-3.366-1.21a2.711 2.711 0 0 0-1.11-1.5c-.907-.637.07-.621.07-.621.317.044.62.163.885.346.266.183.487.426.647.71.135.253.318.476.538.655a2.079 2.079 0 0 0 2.37.196c.045-.52.27-1.006.635-1.37-2.219-.259-4.554-1.138-4.554-5.07a4.022 4.022 0 0 1 1.031-2.75 3.77 3.77 0 0 1 .096-2.713s.839-.275 2.749 1.05a9.26 9.26 0 0 1 5.004 0c1.906-1.325 2.74-1.05 2.74-1.05.37.858.406 1.828.101 2.713a4.017 4.017 0 0 1 1.029 2.75c0 3.939-2.339 4.805-4.564 5.058a2.471 2.471 0 0 1 .679 1.897c0 1.372-.012 2.477-.012 2.814 0 .272.18.592.687.492a10.05 10.05 0 0 0 5.388-4.421 10.473 10.473 0 0 0 1.313-6.948 10.32 10.32 0 0 0-3.39-6.165A9.847 9.847 0 0 0 12.007 2Z"
221+
clipRule="evenodd"
129222
/>
130-
</g>
131-
<defs>
132-
<clipPath id="clip0_13183_10121">
133-
<rect
134-
width="20"
135-
height="20"
136-
fill="white"
137-
transform="translate(0.5)"
138-
/>
139-
</clipPath>
140-
</defs>
141-
</svg>
142-
<span className="text-gray-900">
143-
{t('Continue with Google')}
144-
</span>
145-
</Button>
146-
</div>
147-
<Button
148-
color="gray"
149-
href="#"
150-
className="mt-2 font-bold hover:bg-gray-50"
151-
onClick={() => {
152-
analytic.track('Sign In', {
153-
provider: 'Github',
154-
})
155-
signInWithGithub(['user:email'])
156-
}}
157-
>
158-
<svg
159-
className="w-6 h-6 text-gray-800 dark:text-white"
160-
aria-hidden="true"
161-
xmlns="http://www.w3.org/2000/svg"
162-
width="24"
163-
height="24"
164-
fill="currentColor"
165-
viewBox="0 0 24 24"
166-
>
167-
<path
168-
fillRule="evenodd"
169-
d="M12.006 2a9.847 9.847 0 0 0-6.484 2.44 10.32 10.32 0 0 0-3.393 6.17 10.48 10.48 0 0 0 1.317 6.955 10.045 10.045 0 0 0 5.4 4.418c.504.095.683-.223.683-.494 0-.245-.01-1.052-.014-1.908-2.78.62-3.366-1.21-3.366-1.21a2.711 2.711 0 0 0-1.11-1.5c-.907-.637.07-.621.07-.621.317.044.62.163.885.346.266.183.487.426.647.71.135.253.318.476.538.655a2.079 2.079 0 0 0 2.37.196c.045-.52.27-1.006.635-1.37-2.219-.259-4.554-1.138-4.554-5.07a4.022 4.022 0 0 1 1.031-2.75 3.77 3.77 0 0 1 .096-2.713s.839-.275 2.749 1.05a9.26 9.26 0 0 1 5.004 0c1.906-1.325 2.74-1.05 2.74-1.05.37.858.406 1.828.101 2.713a4.017 4.017 0 0 1 1.029 2.75c0 3.939-2.339 4.805-4.564 5.058a2.471 2.471 0 0 1 .679 1.897c0 1.372-.012 2.477-.012 2.814 0 .272.18.592.687.492a10.05 10.05 0 0 0 5.388-4.421 10.473 10.473 0 0 0 1.313-6.948 10.32 10.32 0 0 0-3.39-6.165A9.847 9.847 0 0 0 12.007 2Z"
170-
clipRule="evenodd"
171-
/>
172-
</svg>
173-
<span className="text-gray-900">{t('Continue with GitHub')}</span>
174-
</Button>
223+
</svg>
224+
<span className="text-gray-900">
225+
{t('Continue with GitHub')}
226+
</span>
227+
</Button>
228+
229+
<div className="mt-4 text-center">
230+
<span className="text-gray-400">
231+
{t('Or sign in with email')}
232+
</span>
233+
</div>
234+
235+
<Button
236+
color="light"
237+
className="mt-2 w-full"
238+
onClick={() => setShowEmailForm(true)}
239+
>
240+
{t('Sign In with Email')}
241+
</Button>
242+
</>
243+
) : (
244+
<form onSubmit={handleEmailSignIn} className="mt-10 space-y-4">
245+
<div>
246+
<Label
247+
htmlFor="email"
248+
value={t('Email address')}
249+
className="text-white"
250+
/>
251+
<TextInput
252+
id="email"
253+
type="email"
254+
value={email}
255+
onChange={(e) => setEmail(e.target.value)}
256+
placeholder={t('Enter your email')}
257+
required
258+
className="mt-1"
259+
/>
260+
</div>
261+
<div>
262+
<Label
263+
htmlFor="password"
264+
value={t('Password')}
265+
className="text-white"
266+
/>
267+
<TextInput
268+
id="password"
269+
type="password"
270+
value={password}
271+
onChange={(e) => setPassword(e.target.value)}
272+
placeholder={t('Enter your password')}
273+
required
274+
className="mt-1"
275+
/>
276+
</div>
277+
<Button
278+
type="submit"
279+
className="w-full"
280+
disabled={emailSignInLoading}
281+
>
282+
{emailSignInLoading ? t('Signing in...') : t('Sign In')}
283+
</Button>
284+
<Button
285+
type="button"
286+
color="light"
287+
className="w-full"
288+
onClick={() => setShowEmailForm(false)}
289+
>
290+
{t('Back to social login')}
291+
</Button>
292+
</form>
293+
)}
175294
</Card>
176295
</div>
177296
</div>

0 commit comments

Comments
 (0)