diff --git a/web-client/package.json b/web-client/package.json index 1358ef7..db0c719 100644 --- a/web-client/package.json +++ b/web-client/package.json @@ -22,8 +22,11 @@ "dependencies": { "@fontsource/bebas-neue": "^5.2.7", "@fontsource/poppins": "^5.2.7", + "@hookform/resolvers": "^5.4.0", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-query": "^5", + "@tanstack/react-query-devtools": "^5.101.0", "axios": "^1.16.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -32,13 +35,16 @@ "radix-ui": "^1.4.3", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-hook-form": "^7.77.0", "react-router-dom": "^7.15.1", "rolldown": "^1.0.2", "rollup": "^4.60.4", - "tslib": "^2.8.1", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.0", - "tw-animate-css": "^1.4.0" + "tslib": "^2.8.1", + "tw-animate-css": "^1.4.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/web-client/pnpm-lock.yaml b/web-client/pnpm-lock.yaml index d681a79..74fdb89 100644 --- a/web-client/pnpm-lock.yaml +++ b/web-client/pnpm-lock.yaml @@ -14,12 +14,21 @@ importers: '@fontsource/poppins': specifier: ^5.2.7 version: 5.2.7 + '@hookform/resolvers': + specifier: ^5.4.0 + version: 5.4.0(react-hook-form@7.77.0(react@19.2.6)) '@radix-ui/react-slot': specifier: ^1.2.4 version: 1.2.4(@types/react@19.2.14)(react@19.2.6) '@tailwindcss/vite': specifier: ^4.3.0 version: 4.3.0(vite@8.0.13(@types/node@24.12.4)(jiti@2.7.0)) + '@tanstack/react-query': + specifier: ^5 + version: 5.101.0(react@19.2.6) + '@tanstack/react-query-devtools': + specifier: ^5.101.0 + version: 5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6) axios: specifier: ^1.16.1 version: 1.16.1 @@ -44,6 +53,9 @@ importers: react-dom: specifier: ^19.2.6 version: 19.2.6(react@19.2.6) + react-hook-form: + specifier: ^7.77.0 + version: 7.77.0(react@19.2.6) react-router-dom: specifier: ^7.15.1 version: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -65,6 +77,12 @@ importers: tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + zod: + specifier: ^4.4.3 + version: 4.4.3 + zustand: + specifier: ^5.0.14 + version: 5.0.14(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) devDependencies: '@eslint/js': specifier: ^10.0.1 @@ -561,6 +579,11 @@ packages: peerDependencies: hono: ^4 + '@hookform/resolvers@5.4.0': + resolution: {integrity: sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1728,6 +1751,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@tailwindcss/node@4.3.0': resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} @@ -1818,6 +1844,23 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/query-core@5.101.0': + resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} + + '@tanstack/query-devtools@5.101.0': + resolution: {integrity: sha512-MVqw17k08RQtGGLEL654+dX/btbX9p/8WjkznO//zusLTMaObxi3Q+MoFwGVkC9K3tqjn8qrrNhJevXx4fJTeQ==} + + '@tanstack/react-query-devtools@5.101.0': + resolution: {integrity: sha512-cpZA0+WqKXwrwMfiWZEGGF6QrIWVQFbhBtxqDF5sQsAfrFf47HIE6fiPbQU3wyAUEN2+7UNqLCQe7oG6m3f93w==} + peerDependencies: + '@tanstack/react-query': ^5.101.0 + react: ^18 || ^19 + + '@tanstack/react-query@5.101.0': + resolution: {integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==} + peerDependencies: + react: ^18 || ^19 + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -3354,6 +3397,12 @@ packages: peerDependencies: react: ^19.2.6 + react-hook-form@7.77.0: + resolution: {integrity: sha512-Sslh9YDYc0GDlWT/lxasnIduNo4v3yyvqRGvmGKUre5AFjDs/HV9/OafHGD8d+sB2yoL4UIL9L8X9i0WlZZebg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -4032,6 +4081,24 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@ampproject/remapping@2.3.0': @@ -4482,6 +4549,11 @@ snapshots: dependencies: hono: 4.12.23 + '@hookform/resolvers@5.4.0(react-hook-form@7.77.0(react@19.2.6))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.77.0(react@19.2.6) + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -5587,6 +5659,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/utils@0.3.0': {} + '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 @@ -5655,6 +5729,21 @@ snapshots: tailwindcss: 4.3.0 vite: 8.0.13(@types/node@24.12.4)(jiti@2.7.0) + '@tanstack/query-core@5.101.0': {} + + '@tanstack/query-devtools@5.101.0': {} + + '@tanstack/react-query-devtools@5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/query-devtools': 5.101.0 + '@tanstack/react-query': 5.101.0(react@19.2.6) + react: 19.2.6 + + '@tanstack/react-query@5.101.0(react@19.2.6)': + dependencies: + '@tanstack/query-core': 5.101.0 + react: 19.2.6 + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -7229,6 +7318,10 @@ snapshots: react: 19.2.6 scheduler: 0.27.0 + react-hook-form@7.77.0(react@19.2.6): + dependencies: + react: 19.2.6 + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.6): dependencies: react: 19.2.6 @@ -7904,3 +7997,9 @@ snapshots: zod@3.25.76: {} zod@4.4.3: {} + + zustand@5.0.14(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) diff --git a/web-client/src/App.tsx b/web-client/src/App.tsx index 872168e..5b18815 100644 --- a/web-client/src/App.tsx +++ b/web-client/src/App.tsx @@ -1,7 +1,6 @@ -import { AppRouter } from '@/app/router/AppRouter' +import { RouterProvider } from 'react-router-dom' +import { router } from '@/app/router/routes' -function App() { - return +export default function App() { + return } - -export default App diff --git a/web-client/src/app/layout/AppShell.tsx b/web-client/src/app/layout/AppShell.tsx new file mode 100644 index 0000000..cd4ba32 --- /dev/null +++ b/web-client/src/app/layout/AppShell.tsx @@ -0,0 +1,79 @@ +import { NavLink, Outlet } from 'react-router-dom' +import { LayoutGrid } from 'lucide-react' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { ThemeToggle } from '@/app/theme/ThemeToggle' +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarProvider, + SidebarTrigger, +} from '@/components/ui/sidebar' + +const NAV_ITEMS = [ + { to: '/members', label: 'Members' }, + { to: '/sport-events', label: 'Sport Events' }, + { to: '/payments', label: 'Payments' }, + { to: '/letters', label: 'Letters' }, + { to: '/organization', label: 'Organization' }, + { to: '/feedback', label: 'Feedback' }, + { to: '/helper', label: 'GenAI Helper' }, +] + +export function AppShell() { + return ( + + + +
+ + Sports Club Platform +
+
+

+ Team Devoops +

+
+
+ + + + {NAV_ITEMS.map(({ to, label }) => ( + + + {({ isActive }) => ( + + {label} + + )} + + + ))} + + + + +
+ +
+
+
+ + +
+ +
+
+ +
+
+ + +
+ ) +} diff --git a/web-client/src/app/router/AppRouter.tsx b/web-client/src/app/router/AppRouter.tsx deleted file mode 100644 index f7047d0..0000000 --- a/web-client/src/app/router/AppRouter.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { useEffect, useState } from 'react' -import { NavLink, Navigate, Route, Routes } from 'react-router-dom' -import { ArrowRight, LayoutGrid, Orbit, Sparkles } from 'lucide-react' -import { getEventsHello } from '@/features/events/api' -import { getFeedbackHello } from '@/features/feedback/api' -import { getLettersHello } from '@/features/letters/api' -import { getMembersHello, getMembersAdminHello } from '@/features/members/api' -import { getOrganizationHello } from '@/features/organization/api' -import { getPaymentsHello } from '@/features/payments/api' -import { ThemeToggle } from '@/app/theme/ThemeToggle' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card' -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarInset, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarProvider, - SidebarTrigger, -} from '@/components/ui/sidebar' - - -type ServicePlaceholderPageProps = { - title: string - loadMessage: () => Promise -} - -function ServicePlaceholderPage({ title, loadMessage }: ServicePlaceholderPageProps) { - const [message, setMessage] = useState(null) - const [error, setError] = useState(null) - const [loading, setLoading] = useState(true) - - useEffect(() => { - let isMounted = true - - loadMessage() - .then((response) => { - if (isMounted) { - setMessage(response) - setError(null) - } - }) - .catch((err: unknown) => { - if (isMounted) { - setMessage(null) - setError(err instanceof Error ? err.message : 'Unknown error') - } - }) - .finally(() => { - if (isMounted) setLoading(false) - }) - - return () => { - isMounted = false - } - }, [loadMessage]) - - return ( -
- - -
- - Connected service -
-
- - {title} - - - The navigation and design system are wired up. This page is still a service - placeholder, but it now lives inside the new Sera-inspired shell and shadcn - components. - -
-
- -
-
-

- Endpoint status -

-
- {loading &&

Loading hello endpoint response...

} - {message &&

{message}

} - {error &&

Failed to load response: {error}

} -
-
-
-
-

- What changed -

-

- DaisyUI primitives are out, semantic tokens and reusable UI building blocks are in. -

-
-
- - Ready for real feature screens using the same component foundation. -
-
-
-
-
- - - - Migration Notes - - The shell is now driven by the Sera theme variables and shadcn components. - - - -
-

- Tokens -

-

Light and dark mode both resolve through CSS variables instead of DaisyUI themes.

-
-
-

- Components -

-

Navigation, cards, and actions use shadcn-style primitives with shared variants.

-
-
-

- Next build step -

-

Feature pages can now expand from this foundation without carrying DaisyUI utility debt forward.

-
-
-
-
- ) -} - -const NAV_ITEMS = [ - { to: '/members', label: 'Members' }, - { to: '/events', label: 'Events' }, - { to: '/payments', label: 'Payments' }, - { to: '/letters', label: 'Letters' }, - { to: '/organization', label: 'Organization' }, - { to: '/feedback', label: 'Feedback' }, -] - -export function AppRouter() { - return ( - - - -
- - Sports Club Platform -
-
-

- Team Devoops -

-
-
- - - - {NAV_ITEMS.map(({ to, label }) => ( - - - {({ isActive }) => ( - - {label} - - )} - - - ))} - - - - -
-
- Active shell - -
- -
-
-
- - -
- -
-
- - } /> - - - - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - -
-
-
- ) -} diff --git a/web-client/src/app/router/routes.tsx b/web-client/src/app/router/routes.tsx new file mode 100644 index 0000000..7899155 --- /dev/null +++ b/web-client/src/app/router/routes.tsx @@ -0,0 +1,26 @@ +import { createBrowserRouter, Navigate } from 'react-router-dom' +import { AppShell } from '@/app/layout/AppShell' +import { MembersPage } from '@/features/members' +import { SportEventsPage } from '@/features/sport-events' +import { PaymentsPage } from '@/features/payments' +import { LettersPage } from '@/features/letters' +import { OrganizationPage } from '@/features/organization' +import { FeedbackPage } from '@/features/feedback' +import { HelperPage } from '@/features/helper' + +export const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { index: true, element: }, + { path: 'members', element: }, + { path: 'sport-events', element: }, + { path: 'payments', element: }, + { path: 'letters', element: }, + { path: 'organization', element: }, + { path: 'feedback', element: }, + { path: 'helper', element: }, + ], + }, +]) diff --git a/web-client/src/components/ui/ErrorMessage.tsx b/web-client/src/components/ui/ErrorMessage.tsx new file mode 100644 index 0000000..faf987e --- /dev/null +++ b/web-client/src/components/ui/ErrorMessage.tsx @@ -0,0 +1,10 @@ +import { AlertCircle } from 'lucide-react' + +export function ErrorMessage({ message }: { message: string }) { + return ( +
+ + {message} +
+ ) +} diff --git a/web-client/src/components/ui/LoadingSpinner.tsx b/web-client/src/components/ui/LoadingSpinner.tsx new file mode 100644 index 0000000..0bb0c4a --- /dev/null +++ b/web-client/src/components/ui/LoadingSpinner.tsx @@ -0,0 +1,7 @@ +export function LoadingSpinner() { + return ( +
+
+
+ ) +} diff --git a/web-client/src/features/events/api.ts b/web-client/src/features/events/api.ts deleted file mode 100644 index e308447..0000000 --- a/web-client/src/features/events/api.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { eventsClient } from '@/features/events/client' - -export async function getEventsHello(): Promise { - const res = await eventsClient.get('/hello') - return res.data -} diff --git a/web-client/src/features/events/client.ts b/web-client/src/features/events/client.ts deleted file mode 100644 index 710d172..0000000 --- a/web-client/src/features/events/client.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createApiClient } from '@/lib/keycloak' - -export const eventsClient = createApiClient('/api/v1/events') diff --git a/web-client/src/features/feedback/client.ts b/web-client/src/features/feedback/api/client.ts similarity index 100% rename from web-client/src/features/feedback/client.ts rename to web-client/src/features/feedback/api/client.ts diff --git a/web-client/src/features/feedback/api.ts b/web-client/src/features/feedback/api/index.ts similarity index 59% rename from web-client/src/features/feedback/api.ts rename to web-client/src/features/feedback/api/index.ts index 80b939f..54f11e9 100644 --- a/web-client/src/features/feedback/api.ts +++ b/web-client/src/features/feedback/api/index.ts @@ -1,6 +1,10 @@ -import { feedbackClient } from '@/features/feedback/client' +export * from './client' +export * from './queries' + +import { feedbackClient } from './client' export async function getFeedbackHello(): Promise { const res = await feedbackClient.get('/hello') + return res.data } diff --git a/web-client/src/features/feedback/api/queries.ts b/web-client/src/features/feedback/api/queries.ts new file mode 100644 index 0000000..d53af6d --- /dev/null +++ b/web-client/src/features/feedback/api/queries.ts @@ -0,0 +1,65 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { feedbackClient } from './client' +import type { Feedback, FeedbackCreate, FeedbackPartialUpdate, FeedbackSummary } from '../types' + +export const feedbackKeys = { + all: ['feedback'] as const, + detail: (id: string) => ['feedback', id] as const, + hello: ['feedback', 'hello'] as const, +} + +export function useFeedbackHello() { + return useQuery({ + queryKey: feedbackKeys.hello, + queryFn: () => feedbackClient.get('/hello').then(r => r.data), + }) +} + +export function useFeedbackList() { + return useQuery({ + queryKey: feedbackKeys.all, + queryFn: () => feedbackClient.get('/').then(r => r.data), + }) +} + +export function useFeedback(id: string) { + return useQuery({ + queryKey: feedbackKeys.detail(id), + queryFn: () => feedbackClient.get(`/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateFeedback() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => feedbackClient.post('/', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: feedbackKeys.all }), + }) +} + +export function useUpdateFeedback() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => feedbackClient.patch(`/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: feedbackKeys.all }) + qc.invalidateQueries({ queryKey: feedbackKeys.detail(id) }) + }, + }) +} + +export function useDeleteFeedback() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => feedbackClient.delete(`/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: feedbackKeys.all }) + qc.removeQueries({ queryKey: feedbackKeys.detail(id) }) + }, + }) +} diff --git a/web-client/src/features/feedback/index.ts b/web-client/src/features/feedback/index.ts new file mode 100644 index 0000000..4144de1 --- /dev/null +++ b/web-client/src/features/feedback/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { FeedbackPage } from './pages/FeedbackPage' diff --git a/web-client/src/features/feedback/pages/FeedbackPage.tsx b/web-client/src/features/feedback/pages/FeedbackPage.tsx new file mode 100644 index 0000000..9ac09d2 --- /dev/null +++ b/web-client/src/features/feedback/pages/FeedbackPage.tsx @@ -0,0 +1,12 @@ +import { useFeedbackHello } from '../api' + +export function FeedbackPage() { + const { data: hello } = useFeedbackHello() + + return ( +
+

Feedback

+ {hello &&

{hello}

} +
+ ) +} diff --git a/web-client/src/features/feedback/types/index.ts b/web-client/src/features/feedback/types/index.ts new file mode 100644 index 0000000..f8224e9 --- /dev/null +++ b/web-client/src/features/feedback/types/index.ts @@ -0,0 +1 @@ +export type { Feedback, FeedbackSummary, FeedbackCreate, FeedbackPartialUpdate } from '@/types' diff --git a/web-client/src/features/helper/api/client.ts b/web-client/src/features/helper/api/client.ts new file mode 100644 index 0000000..6f141c5 --- /dev/null +++ b/web-client/src/features/helper/api/client.ts @@ -0,0 +1,3 @@ +import { createApiClient } from '@/lib/keycloak' + +export const helperClient = createApiClient('/api/v1/helper') diff --git a/web-client/src/features/helper/api/index.ts b/web-client/src/features/helper/api/index.ts new file mode 100644 index 0000000..290cc9f --- /dev/null +++ b/web-client/src/features/helper/api/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './queries' diff --git a/web-client/src/features/helper/api/queries.ts b/web-client/src/features/helper/api/queries.ts new file mode 100644 index 0000000..68abbd3 --- /dev/null +++ b/web-client/src/features/helper/api/queries.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query' + +import { helperClient } from './client' + +export const helperKeys = { + hello: ['helper', 'hello'] as const, + report: (memberId: string) => ['helper', 'report', memberId] as const, +} + +export function useHelperHello() { + return useQuery({ + queryKey: helperKeys.hello, + queryFn: () => helperClient.get('/hello').then(r => r.data), + }) +} + +export function useMemberReport(memberId: string) { + return useQuery({ + queryKey: helperKeys.report(memberId), + queryFn: () => helperClient.get(`/report/${memberId}`).then(r => r.data), + enabled: !!memberId, + }) +} diff --git a/web-client/src/features/helper/index.ts b/web-client/src/features/helper/index.ts new file mode 100644 index 0000000..a4d8d01 --- /dev/null +++ b/web-client/src/features/helper/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { HelperPage } from './pages/HelperPage' diff --git a/web-client/src/features/helper/pages/HelperPage.tsx b/web-client/src/features/helper/pages/HelperPage.tsx new file mode 100644 index 0000000..3fd9cc3 --- /dev/null +++ b/web-client/src/features/helper/pages/HelperPage.tsx @@ -0,0 +1,12 @@ +import { useHelperHello } from '../api' + +export function HelperPage() { + const { data: hello } = useHelperHello() + + return ( +
+

GenAI Helper

+ {hello &&

{hello}

} +
+ ) +} diff --git a/web-client/src/features/helper/types/index.ts b/web-client/src/features/helper/types/index.ts new file mode 100644 index 0000000..c17499c --- /dev/null +++ b/web-client/src/features/helper/types/index.ts @@ -0,0 +1,3 @@ +export interface HelperReport { + content: string +} diff --git a/web-client/src/features/letters/client.ts b/web-client/src/features/letters/api/client.ts similarity index 100% rename from web-client/src/features/letters/client.ts rename to web-client/src/features/letters/api/client.ts diff --git a/web-client/src/features/letters/api.ts b/web-client/src/features/letters/api/index.ts similarity index 59% rename from web-client/src/features/letters/api.ts rename to web-client/src/features/letters/api/index.ts index c6f1b69..9057350 100644 --- a/web-client/src/features/letters/api.ts +++ b/web-client/src/features/letters/api/index.ts @@ -1,6 +1,10 @@ -import { lettersClient } from '@/features/letters/client' +export * from './client' +export * from './queries' + +import { lettersClient } from './client' export async function getLettersHello(): Promise { const res = await lettersClient.get('/hello') + return res.data } diff --git a/web-client/src/features/letters/api/queries.ts b/web-client/src/features/letters/api/queries.ts new file mode 100644 index 0000000..6701028 --- /dev/null +++ b/web-client/src/features/letters/api/queries.ts @@ -0,0 +1,34 @@ +import { useMutation, useQuery } from '@tanstack/react-query' + +import { lettersClient } from './client' +import type { GeneratePdfRequest, SendMailRequest } from '../types' + +export const lettersKeys = { + hello: ['letters', 'hello'] as const, +} + +export function useLettersHello() { + return useQuery({ + queryKey: lettersKeys.hello, + queryFn: () => lettersClient.get('/hello').then(r => r.data), + }) +} + +export function useSendMail() { + return useMutation({ + mutationFn: data => + lettersClient.post('/mail', data.html, { + headers: { 'Content-Type': 'text/html' }, + }).then(() => undefined), + }) +} + +export function useGeneratePdf() { + return useMutation({ + mutationFn: data => + lettersClient.post('/pdf', data.html, { + headers: { 'Content-Type': 'text/html' }, + responseType: 'blob', + }).then(r => r.data), + }) +} diff --git a/web-client/src/features/letters/index.ts b/web-client/src/features/letters/index.ts new file mode 100644 index 0000000..e878b99 --- /dev/null +++ b/web-client/src/features/letters/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { LettersPage } from './pages/LettersPage' diff --git a/web-client/src/features/letters/pages/LettersPage.tsx b/web-client/src/features/letters/pages/LettersPage.tsx new file mode 100644 index 0000000..f2fb8e3 --- /dev/null +++ b/web-client/src/features/letters/pages/LettersPage.tsx @@ -0,0 +1,12 @@ +import { useLettersHello } from '../api' + +export function LettersPage() { + const { data: hello } = useLettersHello() + + return ( +
+

Letters

+ {hello &&

{hello}

} +
+ ) +} diff --git a/web-client/src/features/letters/types/index.ts b/web-client/src/features/letters/types/index.ts new file mode 100644 index 0000000..3317698 --- /dev/null +++ b/web-client/src/features/letters/types/index.ts @@ -0,0 +1,7 @@ +export interface SendMailRequest { + html: string +} + +export interface GeneratePdfRequest { + html: string +} diff --git a/web-client/src/features/members/api.ts b/web-client/src/features/members/api.ts deleted file mode 100644 index d5ef2a9..0000000 --- a/web-client/src/features/members/api.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { membersClient } from '@/features/members/client' - -export async function getMembersHello(): Promise { - const res = await membersClient.get('/hello') - return res.data -} - -export async function getMembersAdminHello(): Promise { - try { - const res = await membersClient.get('/helloAdmin') - return res.data - } - catch { - return "You are not logged into an administrator account" - } -} diff --git a/web-client/src/features/members/client.ts b/web-client/src/features/members/api/client.ts similarity index 100% rename from web-client/src/features/members/client.ts rename to web-client/src/features/members/api/client.ts diff --git a/web-client/src/features/members/api/index.ts b/web-client/src/features/members/api/index.ts new file mode 100644 index 0000000..290cc9f --- /dev/null +++ b/web-client/src/features/members/api/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './queries' diff --git a/web-client/src/features/members/api/queries.ts b/web-client/src/features/members/api/queries.ts new file mode 100644 index 0000000..7a1d1d7 --- /dev/null +++ b/web-client/src/features/members/api/queries.ts @@ -0,0 +1,65 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { membersClient } from './client' +import type { Member, MemberCreate, MemberPartialUpdate, MemberSummary } from '../types' + +export const membersKeys = { + hello: ['members', 'hello'] as const, + all: ['members'] as const, + detail: (id: string) => ['members', id] as const, +} + +export function useMembersHello() { + return useQuery({ + queryKey: membersKeys.hello, + queryFn: () => membersClient.get('/hello').then(r => r.data), + }) +} + +export function useMembers() { + return useQuery({ + queryKey: membersKeys.all, + queryFn: () => membersClient.get('/').then(r => r.data), + }) +} + +export function useMember(id: string) { + return useQuery({ + queryKey: membersKeys.detail(id), + queryFn: () => membersClient.get(`/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateMember() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => membersClient.post('/', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: membersKeys.all }), + }) +} + +export function useUpdateMember() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => membersClient.patch(`/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: membersKeys.all }) + qc.invalidateQueries({ queryKey: membersKeys.detail(id) }) + }, + }) +} + +export function useDeleteMember() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => membersClient.delete(`/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: membersKeys.all }) + qc.removeQueries({ queryKey: membersKeys.detail(id) }) + }, + }) +} diff --git a/web-client/src/features/members/index.ts b/web-client/src/features/members/index.ts new file mode 100644 index 0000000..8fc39e2 --- /dev/null +++ b/web-client/src/features/members/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { MembersPage } from './pages/MembersPage' diff --git a/web-client/src/features/members/pages/MembersPage.tsx b/web-client/src/features/members/pages/MembersPage.tsx new file mode 100644 index 0000000..c12ffd7 --- /dev/null +++ b/web-client/src/features/members/pages/MembersPage.tsx @@ -0,0 +1,12 @@ +import { useMembersHello } from '../api' + +export function MembersPage() { + const { data: hello } = useMembersHello() + + return ( +
+

Members

+ {hello &&

{hello}

} +
+ ) +} diff --git a/web-client/src/features/members/types/index.ts b/web-client/src/features/members/types/index.ts new file mode 100644 index 0000000..1684bbd --- /dev/null +++ b/web-client/src/features/members/types/index.ts @@ -0,0 +1 @@ +export type { Member, MemberSummary, MemberCreate, MemberPartialUpdate } from '@/types' diff --git a/web-client/src/features/organization/client.ts b/web-client/src/features/organization/api/client.ts similarity index 100% rename from web-client/src/features/organization/client.ts rename to web-client/src/features/organization/api/client.ts diff --git a/web-client/src/features/organization/api.ts b/web-client/src/features/organization/api/index.ts similarity index 59% rename from web-client/src/features/organization/api.ts rename to web-client/src/features/organization/api/index.ts index 76f6b0e..ce9415d 100644 --- a/web-client/src/features/organization/api.ts +++ b/web-client/src/features/organization/api/index.ts @@ -1,6 +1,10 @@ -import { organizationClient } from '@/features/organization/client' +export * from './client' +export * from './queries' + +import { organizationClient } from './client' export async function getOrganizationHello(): Promise { const res = await organizationClient.get('/hello') + return res.data } diff --git a/web-client/src/features/organization/api/queries.ts b/web-client/src/features/organization/api/queries.ts new file mode 100644 index 0000000..cbb4378 --- /dev/null +++ b/web-client/src/features/organization/api/queries.ts @@ -0,0 +1,122 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { organizationClient } from './client' +import type { + Sport, + SportCreate, + SportPartialUpdate, + Team, + TeamCreate, + TeamPartialUpdate, +} from '../types' + +export const organizationKeys = { + hello: ['organization', 'hello'] as const, + sports: ['organization', 'sports'] as const, + sport: (name: string) => ['organization', 'sports', name] as const, + teams: ['organization', 'teams'] as const, + team: (id: string) => ['organization', 'teams', id] as const, +} + +export function useOrganizationHello() { + return useQuery({ + queryKey: organizationKeys.hello, + queryFn: () => organizationClient.get('/hello').then(r => r.data), + }) +} + +export function useSports() { + return useQuery({ + queryKey: organizationKeys.sports, + queryFn: () => organizationClient.get('/sports').then(r => r.data), + }) +} + +export function useSport(name: string) { + return useQuery({ + queryKey: organizationKeys.sport(name), + queryFn: () => organizationClient.get(`/sports/${name}`).then(r => r.data), + enabled: !!name, + }) +} + +export function useCreateSport() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => organizationClient.post('/sports', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: organizationKeys.sports }), + }) +} + +export function useUpdateSport() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ name, ...data }) => organizationClient.patch(`/sports/${name}`, data).then(r => r.data), + onSuccess: (_, { name }) => { + qc.invalidateQueries({ queryKey: organizationKeys.sports }) + qc.invalidateQueries({ queryKey: organizationKeys.sport(name) }) + }, + }) +} + +export function useDeleteSport() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: name => organizationClient.delete(`/sports/${name}`).then(() => undefined), + onSuccess: (_, name) => { + qc.invalidateQueries({ queryKey: organizationKeys.sports }) + qc.removeQueries({ queryKey: organizationKeys.sport(name) }) + }, + }) +} + +export function useTeams() { + return useQuery({ + queryKey: organizationKeys.teams, + queryFn: () => organizationClient.get('/teams').then(r => r.data), + }) +} + +export function useTeam(id: string) { + return useQuery({ + queryKey: organizationKeys.team(id), + queryFn: () => organizationClient.get(`/teams/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateTeam() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => organizationClient.post('/teams', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: organizationKeys.teams }), + }) +} + +export function useUpdateTeam() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => organizationClient.patch(`/teams/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: organizationKeys.teams }) + qc.invalidateQueries({ queryKey: organizationKeys.team(id) }) + }, + }) +} + +export function useDeleteTeam() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => organizationClient.delete(`/teams/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: organizationKeys.teams }) + qc.removeQueries({ queryKey: organizationKeys.team(id) }) + }, + }) +} diff --git a/web-client/src/features/organization/index.ts b/web-client/src/features/organization/index.ts new file mode 100644 index 0000000..e74a24f --- /dev/null +++ b/web-client/src/features/organization/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { OrganizationPage } from './pages/OrganizationPage' diff --git a/web-client/src/features/organization/pages/OrganizationPage.tsx b/web-client/src/features/organization/pages/OrganizationPage.tsx new file mode 100644 index 0000000..0b180d8 --- /dev/null +++ b/web-client/src/features/organization/pages/OrganizationPage.tsx @@ -0,0 +1,12 @@ +import { useOrganizationHello } from '../api' + +export function OrganizationPage() { + const { data: hello } = useOrganizationHello() + + return ( +
+

Organization

+ {hello &&

{hello}

} +
+ ) +} diff --git a/web-client/src/features/organization/types/index.ts b/web-client/src/features/organization/types/index.ts new file mode 100644 index 0000000..028d19c --- /dev/null +++ b/web-client/src/features/organization/types/index.ts @@ -0,0 +1 @@ +export type { Sport, SportCreate, SportPartialUpdate, Team, TeamCreate, TeamPartialUpdate } from '@/types' diff --git a/web-client/src/features/payments/client.ts b/web-client/src/features/payments/api/client.ts similarity index 100% rename from web-client/src/features/payments/client.ts rename to web-client/src/features/payments/api/client.ts diff --git a/web-client/src/features/payments/api.ts b/web-client/src/features/payments/api/index.ts similarity index 59% rename from web-client/src/features/payments/api.ts rename to web-client/src/features/payments/api/index.ts index bb6f870..c625700 100644 --- a/web-client/src/features/payments/api.ts +++ b/web-client/src/features/payments/api/index.ts @@ -1,6 +1,10 @@ -import { paymentsClient } from '@/features/payments/client' +export * from './client' +export * from './queries' + +import { paymentsClient } from './client' export async function getPaymentsHello(): Promise { const res = await paymentsClient.get('/hello') + return res.data } diff --git a/web-client/src/features/payments/api/queries.ts b/web-client/src/features/payments/api/queries.ts new file mode 100644 index 0000000..0ef75cf --- /dev/null +++ b/web-client/src/features/payments/api/queries.ts @@ -0,0 +1,82 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { paymentsClient } from './client' +import type { Balance, Transaction, TransactionCreate, TransactionPartialUpdate } from '../types' + +export const paymentsKeys = { + hello: ['payments', 'hello'] as const, + balances: ['payments', 'balances'] as const, + balance: (memberId: string) => ['payments', 'balances', memberId] as const, + transactions: ['payments', 'transactions'] as const, + transaction: (id: string) => ['payments', 'transactions', id] as const, +} + +export function usePaymentsHello() { + return useQuery({ + queryKey: paymentsKeys.hello, + queryFn: () => paymentsClient.get('/hello').then(r => r.data), + }) +} + +export function useBalances() { + return useQuery({ + queryKey: paymentsKeys.balances, + queryFn: () => paymentsClient.get('/balances').then(r => r.data), + }) +} + +export function useMemberBalance(memberId: string) { + return useQuery({ + queryKey: paymentsKeys.balance(memberId), + queryFn: () => paymentsClient.get(`/balances/${memberId}`).then(r => r.data), + enabled: !!memberId, + }) +} + +export function useTransactions() { + return useQuery({ + queryKey: paymentsKeys.transactions, + queryFn: () => paymentsClient.get('/transactions').then(r => r.data), + }) +} + +export function useTransaction(id: string) { + return useQuery({ + queryKey: paymentsKeys.transaction(id), + queryFn: () => paymentsClient.get(`/transactions/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateTransaction() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => paymentsClient.post('/transactions', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: paymentsKeys.transactions }), + }) +} + +export function useUpdateTransaction() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => paymentsClient.patch(`/transactions/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: paymentsKeys.transactions }) + qc.invalidateQueries({ queryKey: paymentsKeys.transaction(id) }) + }, + }) +} + +export function useDeleteTransaction() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => paymentsClient.delete(`/transactions/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: paymentsKeys.transactions }) + qc.removeQueries({ queryKey: paymentsKeys.transaction(id) }) + }, + }) +} diff --git a/web-client/src/features/payments/index.ts b/web-client/src/features/payments/index.ts new file mode 100644 index 0000000..0158ec5 --- /dev/null +++ b/web-client/src/features/payments/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { PaymentsPage } from './pages/PaymentsPage' diff --git a/web-client/src/features/payments/pages/PaymentsPage.tsx b/web-client/src/features/payments/pages/PaymentsPage.tsx new file mode 100644 index 0000000..9ca646b --- /dev/null +++ b/web-client/src/features/payments/pages/PaymentsPage.tsx @@ -0,0 +1,12 @@ +import { usePaymentsHello } from '../api' + +export function PaymentsPage() { + const { data: hello } = usePaymentsHello() + + return ( +
+

Payments

+ {hello &&

{hello}

} +
+ ) +} diff --git a/web-client/src/features/payments/types/index.ts b/web-client/src/features/payments/types/index.ts new file mode 100644 index 0000000..6991f72 --- /dev/null +++ b/web-client/src/features/payments/types/index.ts @@ -0,0 +1 @@ +export type { Transaction, TransactionCreate, TransactionPartialUpdate, Balance } from '@/types' diff --git a/web-client/src/features/sport-events/api/client.ts b/web-client/src/features/sport-events/api/client.ts new file mode 100644 index 0000000..8706cd7 --- /dev/null +++ b/web-client/src/features/sport-events/api/client.ts @@ -0,0 +1,3 @@ +import { createApiClient } from '@/lib/keycloak' + +export const sportEventsClient = createApiClient('/api/v1/events') diff --git a/web-client/src/features/sport-events/api/index.ts b/web-client/src/features/sport-events/api/index.ts new file mode 100644 index 0000000..290cc9f --- /dev/null +++ b/web-client/src/features/sport-events/api/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './queries' diff --git a/web-client/src/features/sport-events/api/queries.ts b/web-client/src/features/sport-events/api/queries.ts new file mode 100644 index 0000000..97794bc --- /dev/null +++ b/web-client/src/features/sport-events/api/queries.ts @@ -0,0 +1,65 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { sportEventsClient } from './client' +import type { SportEvent, EventCreate, EventPartialUpdate, EventSummary } from '../types' + +export const sportEventsKeys = { + hello: ['sport-events', 'hello'] as const, + all: ['sport-events'] as const, + detail: (id: string) => ['sport-events', id] as const, +} + +export function useSportEventsHello() { + return useQuery({ + queryKey: sportEventsKeys.hello, + queryFn: () => sportEventsClient.get('/hello').then(r => r.data), + }) +} + +export function useSportEvents() { + return useQuery({ + queryKey: sportEventsKeys.all, + queryFn: () => sportEventsClient.get('/').then(r => r.data), + }) +} + +export function useSportEvent(id: string) { + return useQuery({ + queryKey: sportEventsKeys.detail(id), + queryFn: () => sportEventsClient.get(`/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateSportEvent() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => sportEventsClient.post('/', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: sportEventsKeys.all }), + }) +} + +export function useUpdateSportEvent() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => sportEventsClient.patch(`/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: sportEventsKeys.all }) + qc.invalidateQueries({ queryKey: sportEventsKeys.detail(id) }) + }, + }) +} + +export function useDeleteSportEvent() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => sportEventsClient.delete(`/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: sportEventsKeys.all }) + qc.removeQueries({ queryKey: sportEventsKeys.detail(id) }) + }, + }) +} diff --git a/web-client/src/features/sport-events/index.ts b/web-client/src/features/sport-events/index.ts new file mode 100644 index 0000000..5be00ba --- /dev/null +++ b/web-client/src/features/sport-events/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { SportEventsPage } from './pages/SportEventsPage' diff --git a/web-client/src/features/sport-events/pages/SportEventsPage.tsx b/web-client/src/features/sport-events/pages/SportEventsPage.tsx new file mode 100644 index 0000000..52b70cf --- /dev/null +++ b/web-client/src/features/sport-events/pages/SportEventsPage.tsx @@ -0,0 +1,12 @@ +import { useSportEventsHello } from '../api' + +export function SportEventsPage() { + const { data: hello } = useSportEventsHello() + + return ( +
+

Events

+ {hello &&

{hello}

} +
+ ) +} diff --git a/web-client/src/features/sport-events/types/index.ts b/web-client/src/features/sport-events/types/index.ts new file mode 100644 index 0000000..b29331c --- /dev/null +++ b/web-client/src/features/sport-events/types/index.ts @@ -0,0 +1 @@ +export type { SportEvent, EventSummary, EventCreate, EventPartialUpdate } from '@/types' diff --git a/web-client/src/lib/forms.ts b/web-client/src/lib/forms.ts new file mode 100644 index 0000000..edefd82 --- /dev/null +++ b/web-client/src/lib/forms.ts @@ -0,0 +1,3 @@ +export { useForm } from 'react-hook-form' +export { zodResolver } from '@hookform/resolvers/zod' +export { z } from 'zod' diff --git a/web-client/src/main.tsx b/web-client/src/main.tsx index a129fd4..c786e9f 100644 --- a/web-client/src/main.tsx +++ b/web-client/src/main.tsx @@ -1,10 +1,31 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { BrowserRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { AxiosError } from 'axios' import keycloak from '@/lib/keycloak' +import { ThemeProvider } from '@/app/theme/ThemeProvider' import '@/index.css' import App from './App.tsx' -import { ThemeProvider } from './app/theme/ThemeProvider' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + retry: (failureCount, error) => { + if ( + error instanceof AxiosError && + error.response && + error.response.status >= 400 && + error.response.status < 500 + ) { + return false + } + return failureCount < 2 + }, + }, + }, +}) keycloak.init({ onLoad: 'login-required', pkceMethod: 'S256' }).then((authenticated) => { if (!authenticated) { @@ -15,9 +36,9 @@ keycloak.init({ onLoad: 'login-required', pkceMethod: 'S256' }).then((authentica createRoot(document.getElementById('root')!).render( - + - + , ) diff --git a/web-client/src/store/ui.ts b/web-client/src/store/ui.ts new file mode 100644 index 0000000..8337041 --- /dev/null +++ b/web-client/src/store/ui.ts @@ -0,0 +1,11 @@ +import { create } from 'zustand' + +interface UIState { + activeTab: string | null + setActiveTab: (tab: string | null) => void +} + +export const useUIStore = create((set) => ({ + activeTab: null, + setActiveTab: (tab) => set({ activeTab: tab }), +})) diff --git a/web-client/src/types.ts b/web-client/src/types.ts new file mode 100644 index 0000000..ca5bb0b --- /dev/null +++ b/web-client/src/types.ts @@ -0,0 +1,31 @@ +import type { components } from './api' + +type S = components['schemas'] + +export type Member = S['Member'] +export type MemberSummary = S['MemberSummary'] +export type MemberCreate = S['MemberCreate'] +export type MemberPartialUpdate = S['MemberPartialUpdate'] + +export type SportEvent = S['Event'] +export type EventSummary = S['EventSummary'] +export type EventCreate = S['EventCreate'] +export type EventPartialUpdate = S['EventPartialUpdate'] + +export type Sport = S['Sport'] +export type SportCreate = S['SportCreate'] +export type SportPartialUpdate = S['SportPartialUpdate'] + +export type Team = S['Team'] +export type TeamCreate = S['TeamCreate'] +export type TeamPartialUpdate = S['TeamPartialUpdate'] + +export type Feedback = S['Feedback'] +export type FeedbackSummary = S['FeedbackSummary'] +export type FeedbackCreate = S['FeedbackCreate'] +export type FeedbackPartialUpdate = S['FeedbackPartialUpdate'] + +export type Transaction = S['Transaction'] +export type TransactionCreate = S['TransactionCreate'] +export type TransactionPartialUpdate = S['TransactionPartialUpdate'] +export type Balance = S['Balance']