Compare commits
No commits in common. "1710d5d91d577470a62134da0e8228b2db6d2296" and "5e96ab6955d8a021dcf6c1ed3c52b4d813fb6198" have entirely different histories.
1710d5d91d
...
5e96ab6955
10 changed files with 67 additions and 59 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { AuthService } from '../../../auth/authService.ts'
|
import { AuthService } from '../../../auth/authService.ts'
|
||||||
import { useEffect, useState, useRef, MouseEvent, useCallback } from 'react'
|
import { useEffect, useState, useRef, MouseEvent } from 'react'
|
||||||
import { SignupCode } from '../../../auth/signupCode.ts'
|
import { SignupCode } from '../../../auth/signupCode.ts'
|
||||||
import { Temporal } from '@js-temporal/polyfill'
|
import { Temporal } from '@js-temporal/polyfill'
|
||||||
import Button from '../../../../components/buttons/Button.tsx'
|
import Button from '../../../../components/buttons/Button.tsx'
|
||||||
|
@ -12,24 +12,25 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa
|
||||||
const [codes, setCodes] = useState<SignupCode[]>([])
|
const [codes, setCodes] = useState<SignupCode[]>([])
|
||||||
const [code, setCode] = useState('')
|
const [code, setCode] = useState('')
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const dialogRef = useRef<HTMLDialogElement>(null)
|
const dialogRef = useRef<HTMLDialogElement>(null)
|
||||||
const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null)
|
const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null)
|
||||||
const [activeCode, setActiveCode] = useState<string | null>(null)
|
const [activeCode, setActiveCode] = useState<string | null>(null)
|
||||||
|
|
||||||
const fetchCodes = useCallback(async () => {
|
const fetchCodes = async () => {
|
||||||
try {
|
try {
|
||||||
setCodes(await authService.listSignupCodes())
|
setCodes(await authService.listSignupCodes())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch signup codes:', err)
|
console.error('Failed to fetch signup codes:', err)
|
||||||
}
|
}
|
||||||
}, [authService])
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutId = setTimeout(fetchCodes)
|
const timeoutId = setTimeout(fetchCodes)
|
||||||
return () => clearTimeout(timeoutId)
|
return () => clearTimeout(timeoutId)
|
||||||
}, [authService, fetchCodes])
|
}, [authService])
|
||||||
|
|
||||||
const handleCreateCode = async (e: React.FormEvent) => {
|
const handleCreateCode = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -37,11 +38,12 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await authService.createSignupCode(code, name)
|
await authService.createSignupCode(code, email, name)
|
||||||
setCode('')
|
setCode('')
|
||||||
setName('')
|
setName('')
|
||||||
|
setEmail('')
|
||||||
dialogRef.current?.close()
|
dialogRef.current?.close()
|
||||||
fetchCodes()
|
fetchCodes() // Refresh the table
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to create signup code')
|
setError(err instanceof Error ? err.message : 'Failed to create signup code')
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -114,6 +116,7 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left">Code</th>
|
<th className="text-left">Code</th>
|
||||||
|
<th className="text-left">Email</th>
|
||||||
<th className="text-left">Redeemed By</th>
|
<th className="text-left">Redeemed By</th>
|
||||||
<th className="text-left">Expires On</th>
|
<th className="text-left">Expires On</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -131,6 +134,7 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa
|
||||||
{code.code}
|
{code.code}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{code.email}</td>
|
||||||
<td>{code.redeemedBy || 'Not redeemed'}</td>
|
<td>{code.redeemedBy || 'Not redeemed'}</td>
|
||||||
<td>{formatDate(code.expiresOn)}</td>
|
<td>{formatDate(code.expiresOn)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -187,6 +191,19 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
<Button type="button" onClick={closeDialog} secondary>
|
<Button type="button" onClick={closeDialog} secondary>
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
@ -19,12 +19,7 @@ export class AuthService {
|
||||||
dispatchMessage('auth:logged-in', null)
|
dispatchMessage('auth:logged-in', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
async signup(
|
async signup(username: string, password: string, signupCode: string, rememberMe: boolean = false) {
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
signupCode: string,
|
|
||||||
rememberMe: boolean = false,
|
|
||||||
) {
|
|
||||||
const res = await this.client.POST('/auth/register', {
|
const res = await this.client.POST('/auth/register', {
|
||||||
body: { username, password, signupCode, email: null, rememberMe },
|
body: { username, password, signupCode, email: null, rememberMe },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
@ -44,9 +39,9 @@ export class AuthService {
|
||||||
dispatchMessage('auth:logged-out', null)
|
dispatchMessage('auth:logged-out', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSignupCode(code: string, name: string) {
|
async createSignupCode(code: string, email: string, name: string) {
|
||||||
const res = await this.client.POST('/auth/signup-codes', {
|
const res = await this.client.POST('/auth/signup-codes', {
|
||||||
body: { code, name },
|
body: { code, email, name },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useUser } from '../../user/user.ts'
|
import { useUser } from '../../user/user.ts'
|
||||||
import NavButton from '../../../components/buttons/NavButton.tsx'
|
import NavButton from '../../../components/buttons/NavButton.tsx'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useTranslations } from '../../i18n/translations.ts'
|
import { useTranslations } from '../../i18n/useTranslations.ts'
|
||||||
|
|
||||||
export default function AuthNavButtons() {
|
export default function AuthNavButtons() {
|
||||||
const { t } = useTranslations()
|
const { t } = useTranslations()
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useUser } from '../../user/user.ts'
|
||||||
import NavBar from '../../../components/NavBar.tsx'
|
import NavBar from '../../../components/NavBar.tsx'
|
||||||
import NavButton from '../../../components/buttons/NavButton.tsx'
|
import NavButton from '../../../components/buttons/NavButton.tsx'
|
||||||
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
||||||
import { useTranslations } from '../../i18n/translations.ts'
|
import { useTranslations } from '../../i18n/useTranslations.ts'
|
||||||
|
|
||||||
interface LoginPageProps {
|
interface LoginPageProps {
|
||||||
authService: AuthService
|
authService: AuthService
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { AuthService } from '../authService.ts'
|
||||||
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
||||||
import NavBar from '../../../components/NavBar.tsx'
|
import NavBar from '../../../components/NavBar.tsx'
|
||||||
import NavButton from '../../../components/buttons/NavButton.tsx'
|
import NavButton from '../../../components/buttons/NavButton.tsx'
|
||||||
import { useTranslations } from '../../i18n/translations.ts'
|
import { useTranslations } from '../../i18n/useTranslations.ts'
|
||||||
|
|
||||||
const SignupCodeKey = 'signupCode'
|
const SignupCodeKey = 'signupCode'
|
||||||
|
|
||||||
|
|
24
src/app/i18n/translationKeys.ts
Normal file
24
src/app/i18n/translationKeys.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export interface Translations {
|
||||||
|
'auth.login.cta': string
|
||||||
|
'auth.login.register_instead': string
|
||||||
|
'auth.password.label': string
|
||||||
|
'auth.register.cta': string
|
||||||
|
'auth.register.login_instead': string
|
||||||
|
'auth.remember_me.label': string
|
||||||
|
'auth.username.label': string
|
||||||
|
|
||||||
|
'misc.loading': string
|
||||||
|
|
||||||
|
'nav.admin': string
|
||||||
|
'nav.home': string
|
||||||
|
'nav.login': string
|
||||||
|
'nav.logout': string
|
||||||
|
'nav.register': string
|
||||||
|
|
||||||
|
'post.add_media.cta': string
|
||||||
|
'post.editor.placeholder': string
|
||||||
|
'post.public.label': string
|
||||||
|
'post.submit.cta': string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TranslationKey = keyof Translations
|
|
@ -1,41 +0,0 @@
|
||||||
import en from './translations/en.json' assert { type: 'json' }
|
|
||||||
|
|
||||||
interface Translation {
|
|
||||||
'auth.login.cta': string
|
|
||||||
'auth.login.register_instead': string
|
|
||||||
'auth.password.label': string
|
|
||||||
'auth.register.cta': string
|
|
||||||
'auth.register.login_instead': string
|
|
||||||
'auth.remember_me.label': string
|
|
||||||
'auth.username.label': string
|
|
||||||
|
|
||||||
'misc.loading': string
|
|
||||||
|
|
||||||
'nav.admin': string
|
|
||||||
'nav.home': string
|
|
||||||
'nav.login': string
|
|
||||||
'nav.logout': string
|
|
||||||
'nav.register': string
|
|
||||||
|
|
||||||
'post.add_media.cta': string
|
|
||||||
'post.editor.placeholder': string
|
|
||||||
'post.public.label': string
|
|
||||||
'post.submit.cta': string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TranslationKey = keyof Translation
|
|
||||||
|
|
||||||
export interface UseTranslations {
|
|
||||||
t: <K extends TranslationKey>(key: K) => Translation[K]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTranslations(): UseTranslations {
|
|
||||||
// TODO somehow handle other languages (reactively)
|
|
||||||
const texts = en as Translation
|
|
||||||
|
|
||||||
function getText<K extends TranslationKey>(key: K): Translation[K] {
|
|
||||||
return texts[key] ?? key
|
|
||||||
}
|
|
||||||
|
|
||||||
return { t: getText }
|
|
||||||
}
|
|
13
src/app/i18n/useTranslations.ts
Normal file
13
src/app/i18n/useTranslations.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { TranslationKey, Translations } from './translationKeys.ts'
|
||||||
|
import en from './en.json' assert { type: 'json' }
|
||||||
|
|
||||||
|
export function useTranslations() {
|
||||||
|
// TODO somehow handle other languages (reactively)
|
||||||
|
const texts = en as Translations
|
||||||
|
|
||||||
|
function getText<K extends TranslationKey>(key: K): Translations[K] {
|
||||||
|
return texts[key] ?? key
|
||||||
|
}
|
||||||
|
|
||||||
|
return { t: getText }
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import FancyTextEditor, { TextInputKeyDownEvent } from './inputs/FancyTextEditor
|
||||||
import Button from './buttons/Button.tsx'
|
import Button from './buttons/Button.tsx'
|
||||||
import { openFileDialog } from '../utils/openFileDialog.ts'
|
import { openFileDialog } from '../utils/openFileDialog.ts'
|
||||||
import makePica from 'pica'
|
import makePica from 'pica'
|
||||||
import { useTranslations } from '../app/i18n/translations.ts'
|
import { useTranslations } from '../app/i18n/useTranslations.ts'
|
||||||
|
|
||||||
interface NewPostWidgetProps {
|
interface NewPostWidgetProps {
|
||||||
onSubmit: (
|
onSubmit: (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue