diff --git a/src/App.tsx b/src/App.tsx index cb89675..74cc867 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,8 @@ import LoginPage from './app/auth/pages/LoginPage.tsx' import { AuthService } from './app/auth/authService.ts' import LogoutPage from './app/auth/pages/LogoutPage.tsx' import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' +import AdminPage from './app/admin/pages/AdminPage.tsx' +import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx' function App() { const postService = new PostsService() @@ -26,6 +28,12 @@ function App() { } /> } /> } /> + }> + } + /> + diff --git a/src/app/admin/pages/AdminPage.tsx b/src/app/admin/pages/AdminPage.tsx new file mode 100644 index 0000000..18fa310 --- /dev/null +++ b/src/app/admin/pages/AdminPage.tsx @@ -0,0 +1,24 @@ +import NavBar from '../../../components/NavBar' +import NavButton from '../../../components/buttons/NavButton' +import { Outlet } from 'react-router-dom' + +export default function AdminPage() { + return ( +
+ + home + + +
+ +
+ +
+
+
+ ) +} diff --git a/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx b/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx new file mode 100644 index 0000000..a04bb7f --- /dev/null +++ b/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx @@ -0,0 +1,187 @@ +import { AuthService } from '../../../auth/authService.ts' +import { useEffect, useState, useRef } from 'react' +import { SignupCode } from '../../../auth/signupCode.ts' +import { Temporal } from '@js-temporal/polyfill' +import Button from '../../../../components/buttons/Button.tsx' + +interface SignupCodesManagementPageProps { + authService: AuthService +} + +export default function SignupCodesManagementPage({ authService }: SignupCodesManagementPageProps) { + const [codes, setCodes] = useState([]) + const [code, setCode] = useState('') + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const dialogRef = useRef(null) + + const fetchCodes = async () => { + try { + setCodes(await authService.listSignupCodes()) + } catch (err) { + console.error('Failed to fetch signup codes:', err) + } + } + + useEffect(() => { + const timeoutId = setTimeout(fetchCodes) + return () => clearTimeout(timeoutId) + }, [authService]) + + const handleCreateCode = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + try { + await authService.createSignupCode(code, email, name) + setCode('') + setName('') + setEmail('') + dialogRef.current?.close() + fetchCodes() // Refresh the table + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create signup code') + } finally { + setIsLoading(false) + } + } + + const openDialog = () => { + dialogRef.current?.showModal() + } + + const closeDialog = () => { + dialogRef.current?.close() + setError(null) + } + + const formatDate = (date: Temporal.Instant | null) => { + if (!date) return 'Never' + try { + // Convert Temporal.Instant to a JavaScript Date for formatting + const jsDate = new Date(date.epochMilliseconds) + + // Format as: "Jan 1, 2023, 12:00 PM" + return jsDate.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } catch (err: unknown) { + console.error(err) + return date.toString() + } + } + + return ( + <> +
+

Signup Codes

+ +
+ +
+ + + + + + + + + + + {codes.map((code) => ( + + + + + + + ))} + {codes.length === 0 && ( + + + + )} + +
CodeEmailRedeemed ByExpires On
+ {code.code} + {code.email}{code.redeemedBy || 'Not redeemed'}{formatDate(code.expiresOn)}
+ No signup codes found +
+
+ + +

Create New Signup Code

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setCode(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md" + required + /> +
+ +
+ + setName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md" + required + /> +
+ +
+ + setEmail(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md" + required + /> +
+ +
+ + +
+
+
+ + ) +} diff --git a/src/app/api/schema.ts b/src/app/api/schema.ts index 66b0ed1..32d4ad8 100644 --- a/src/app/api/schema.ts +++ b/src/app/api/schema.ts @@ -87,7 +87,8 @@ export interface paths { requestBody: { content: { 'multipart/form-data': { - file?: components['schemas']['IFormFile'] + /** Format: binary */ + file?: string } } } @@ -361,8 +362,6 @@ export interface components { /** Format: uuid */ next: string | null } - /** Format: binary */ - IFormFile: string ListSignupCodesResult: { signupCodes: components['schemas']['SignupCodeDto'][] } @@ -374,6 +373,7 @@ export interface components { /** Format: uuid */ userId: string username: string + isSuperUser: boolean } PostAuthorDto: { /** Format: uuid */ @@ -407,6 +407,7 @@ export interface components { /** Format: uuid */ userId: string username: string + isSuperUser: boolean } SignupCodeDto: { code: string diff --git a/src/app/auth/authService.ts b/src/app/auth/authService.ts index 2fb7ed9..a5733d6 100644 --- a/src/app/auth/authService.ts +++ b/src/app/auth/authService.ts @@ -1,6 +1,7 @@ import { dispatchMessage } from '../messageBus/messageBus.ts' import client from '../api/client.ts' import { ProblemDetails } from '../../types' +import { SignupCode } from './signupCode.ts' export class AuthService { constructor() {} @@ -60,6 +61,6 @@ export class AuthService { throw new Error('error') } - return res.data.signupCodes + return res.data.signupCodes.map(SignupCode.fromDto) } } diff --git a/src/app/auth/signupCode.ts b/src/app/auth/signupCode.ts new file mode 100644 index 0000000..96801b5 --- /dev/null +++ b/src/app/auth/signupCode.ts @@ -0,0 +1,20 @@ +import { Temporal } from '@js-temporal/polyfill' +import { components } from '../api/schema.ts' + +export class SignupCode { + constructor( + public readonly code: string, + public readonly email: string, + public readonly redeemedBy: string | null, + public readonly expiresOn: Temporal.Instant | null, + ) {} + + static fromDto(dto: components['schemas']['SignupCodeDto']): SignupCode { + return new SignupCode( + dto.code, + dto.email, + dto.redeemingUsername, + dto.expiresOn ? Temporal.Instant.from(dto.expiresOn) : null, + ) + } +} diff --git a/src/app/messageBus/messageTypes.ts b/src/app/messageBus/messageTypes.ts index 3ecfdcb..8d15f6c 100644 --- a/src/app/messageBus/messageTypes.ts +++ b/src/app/messageBus/messageTypes.ts @@ -1,12 +1,8 @@ +import { User } from '../user/userStore.ts' + export interface MessageTypes { - 'auth:logged-in': { - userId: string - username: string - } - 'auth:registered': { - userId: string - username: string - } + 'auth:logged-in': User + 'auth:registered': User 'auth:logged-out': null 'auth:unauthorized': null } diff --git a/src/app/user/userStore.ts b/src/app/user/userStore.ts index 052de79..49107fc 100644 --- a/src/app/user/userStore.ts +++ b/src/app/user/userStore.ts @@ -4,6 +4,7 @@ import { addMessageListener } from '../messageBus/messageBus.ts' export interface User { userId: string username: string + isSuperUser: boolean } export type UserStore = Store @@ -16,23 +17,10 @@ userStore.subscribe((user) => { localStorage.setItem(UserKey, JSON.stringify(user)) }) -addMessageListener('auth:logged-in', (e) => { - userStore.setState({ - userId: e.userId, - username: e.username, - }) -}) - -addMessageListener('auth:registered', (e) => { - userStore.setState({ - userId: e.userId, - username: e.username, - }) -}) - -addMessageListener('auth:logged-out', () => { - userStore.setState(null) -}) +const setUser = (u: User | null) => userStore.setState(u) +addMessageListener('auth:logged-in', setUser) +addMessageListener('auth:registered', setUser) +addMessageListener('auth:logged-out', setUser) export const useUser = () => { const [user] = useStore(userStore) diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 265e525..5dc99dd 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -1,9 +1,15 @@ import { PropsWithChildren } from 'react' +import { useUser } from '../app/user/userStore.ts' +import NavButton from './buttons/NavButton.tsx' type NavBarProps = unknown export default function NavBar({ children }: PropsWithChildren) { + const { user } = useUser() return ( - + ) } diff --git a/src/components/buttons/NavButton.tsx b/src/components/buttons/NavButton.tsx index fd2fe67..1ae5373 100644 --- a/src/components/buttons/NavButton.tsx +++ b/src/components/buttons/NavButton.tsx @@ -2,6 +2,7 @@ import { PropsWithChildren } from 'react' import { Link } from 'react-router-dom' interface NavLinkButtonProps { to: string | To + className?: string } interface To { @@ -10,9 +11,13 @@ interface To { hash?: string } -export default function NavButton({ to, children }: PropsWithChildren) { +export default function NavButton({ + to, + className: extraClasses = '', + children, +}: PropsWithChildren) { return ( - + {children} )