From 61001e2f47c332b6c8260ed9457b752cc07e94df Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 1/7] chore: add tanstack query, zustand, react-hook-form, and zod deps --- web-client/package.json | 10 +++- web-client/pnpm-lock.yaml | 99 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/web-client/package.json b/web-client/package.json index 404ffb7..2ea5990 100644 --- a/web-client/package.json +++ b/web-client/package.json @@ -21,8 +21,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", @@ -31,13 +34,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 a01df50..bac5911 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 @@ -551,6 +569,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'} @@ -1706,6 +1729,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==} @@ -1796,6 +1822,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==} @@ -3256,6 +3299,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'} @@ -3918,6 +3967,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: '@asamuzakjp/css-color@3.2.0': @@ -4361,6 +4428,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 @@ -5452,6 +5524,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 @@ -5520,6 +5594,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 @@ -7002,6 +7091,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 @@ -7655,3 +7748,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) From c8cc872e3539cc68b5ad5468a638172630a93472 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 2/7] feat: add app shell layout and migrate to route-object router --- web-client/src/App.tsx | 9 +- web-client/src/app/layout/AppShell.tsx | 79 ++++++++ web-client/src/app/router/AppRouter.tsx | 241 ------------------------ web-client/src/app/router/routes.tsx | 26 +++ web-client/src/main.tsx | 29 ++- 5 files changed, 134 insertions(+), 250 deletions(-) create mode 100644 web-client/src/app/layout/AppShell.tsx delete mode 100644 web-client/src/app/router/AppRouter.tsx create mode 100644 web-client/src/app/router/routes.tsx 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/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( - + - + , ) From ad231bae964e39036c2f91be26565cce921acfbe Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 3/7] feat: add shared types bridge, form lib, zustand store, and ui primitives --- web-client/src/components/ui/ErrorMessage.tsx | 10 ++++++ .../src/components/ui/LoadingSpinner.tsx | 7 +++++ web-client/src/lib/forms.ts | 3 ++ web-client/src/store/ui.ts | 11 +++++++ web-client/src/types.ts | 31 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 web-client/src/components/ui/ErrorMessage.tsx create mode 100644 web-client/src/components/ui/LoadingSpinner.tsx create mode 100644 web-client/src/lib/forms.ts create mode 100644 web-client/src/store/ui.ts create mode 100644 web-client/src/types.ts 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/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/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'] From 84c19a6f133fa9171dff7cd6898e3e0a508bc8d2 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 4/7] feat: restructure features into feature-sliced design with query hooks --- .../src/features/feedback/{ => api}/client.ts | 0 .../feedback/{api.ts => api/index.ts} | 6 +- .../src/features/feedback/api/queries.ts | 57 +++++++++ web-client/src/features/feedback/index.ts | 3 + .../features/feedback/pages/FeedbackPage.tsx | 7 ++ .../src/features/feedback/types/index.ts | 1 + web-client/src/features/helper/api/client.ts | 3 + web-client/src/features/helper/api/index.ts | 2 + web-client/src/features/helper/api/queries.ts | 15 +++ web-client/src/features/helper/index.ts | 3 + .../src/features/helper/pages/HelperPage.tsx | 7 ++ web-client/src/features/helper/types/index.ts | 3 + .../src/features/letters/{ => api}/client.ts | 0 .../features/letters/{api.ts => api/index.ts} | 6 +- .../src/features/letters/api/queries.ts | 23 ++++ web-client/src/features/letters/index.ts | 3 + .../features/letters/pages/LettersPage.tsx | 7 ++ .../src/features/letters/types/index.ts | 7 ++ web-client/src/features/members/api.ts | 16 --- .../src/features/members/{ => api}/client.ts | 0 web-client/src/features/members/api/index.ts | 2 + .../src/features/members/api/queries.ts | 57 +++++++++ web-client/src/features/members/index.ts | 3 + .../features/members/pages/MembersPage.tsx | 7 ++ .../src/features/members/types/index.ts | 1 + .../features/organization/{ => api}/client.ts | 0 .../organization/{api.ts => api/index.ts} | 6 +- .../src/features/organization/api/queries.ts | 114 ++++++++++++++++++ web-client/src/features/organization/index.ts | 3 + .../organization/pages/OrganizationPage.tsx | 7 ++ .../src/features/organization/types/index.ts | 1 + .../src/features/payments/api/client.ts | 3 + .../payments/{api.ts => api/index.ts} | 6 +- .../src/features/payments/api/queries.ts | 74 ++++++++++++ web-client/src/features/payments/client.ts | 3 - web-client/src/features/payments/index.ts | 3 + .../features/payments/pages/PaymentsPage.tsx | 7 ++ .../src/features/payments/types/index.ts | 1 + .../src/features/sport-events/api/client.ts | 3 + .../src/features/sport-events/api/index.ts | 2 + .../src/features/sport-events/api/queries.ts | 57 +++++++++ web-client/src/features/sport-events/index.ts | 3 + .../sport-events/pages/SportEventsPage.tsx | 7 ++ .../src/features/sport-events/types/index.ts | 1 + 44 files changed, 517 insertions(+), 23 deletions(-) rename web-client/src/features/feedback/{ => api}/client.ts (100%) rename web-client/src/features/feedback/{api.ts => api/index.ts} (59%) create mode 100644 web-client/src/features/feedback/api/queries.ts create mode 100644 web-client/src/features/feedback/index.ts create mode 100644 web-client/src/features/feedback/pages/FeedbackPage.tsx create mode 100644 web-client/src/features/feedback/types/index.ts create mode 100644 web-client/src/features/helper/api/client.ts create mode 100644 web-client/src/features/helper/api/index.ts create mode 100644 web-client/src/features/helper/api/queries.ts create mode 100644 web-client/src/features/helper/index.ts create mode 100644 web-client/src/features/helper/pages/HelperPage.tsx create mode 100644 web-client/src/features/helper/types/index.ts rename web-client/src/features/letters/{ => api}/client.ts (100%) rename web-client/src/features/letters/{api.ts => api/index.ts} (59%) create mode 100644 web-client/src/features/letters/api/queries.ts create mode 100644 web-client/src/features/letters/index.ts create mode 100644 web-client/src/features/letters/pages/LettersPage.tsx create mode 100644 web-client/src/features/letters/types/index.ts delete mode 100644 web-client/src/features/members/api.ts rename web-client/src/features/members/{ => api}/client.ts (100%) create mode 100644 web-client/src/features/members/api/index.ts create mode 100644 web-client/src/features/members/api/queries.ts create mode 100644 web-client/src/features/members/index.ts create mode 100644 web-client/src/features/members/pages/MembersPage.tsx create mode 100644 web-client/src/features/members/types/index.ts rename web-client/src/features/organization/{ => api}/client.ts (100%) rename web-client/src/features/organization/{api.ts => api/index.ts} (59%) create mode 100644 web-client/src/features/organization/api/queries.ts create mode 100644 web-client/src/features/organization/index.ts create mode 100644 web-client/src/features/organization/pages/OrganizationPage.tsx create mode 100644 web-client/src/features/organization/types/index.ts create mode 100644 web-client/src/features/payments/api/client.ts rename web-client/src/features/payments/{api.ts => api/index.ts} (59%) create mode 100644 web-client/src/features/payments/api/queries.ts delete mode 100644 web-client/src/features/payments/client.ts create mode 100644 web-client/src/features/payments/index.ts create mode 100644 web-client/src/features/payments/pages/PaymentsPage.tsx create mode 100644 web-client/src/features/payments/types/index.ts create mode 100644 web-client/src/features/sport-events/api/client.ts create mode 100644 web-client/src/features/sport-events/api/index.ts create mode 100644 web-client/src/features/sport-events/api/queries.ts create mode 100644 web-client/src/features/sport-events/index.ts create mode 100644 web-client/src/features/sport-events/pages/SportEventsPage.tsx create mode 100644 web-client/src/features/sport-events/types/index.ts 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..4e5e67e --- /dev/null +++ b/web-client/src/features/feedback/api/queries.ts @@ -0,0 +1,57 @@ +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, +} + +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..7de50c3 --- /dev/null +++ b/web-client/src/features/feedback/pages/FeedbackPage.tsx @@ -0,0 +1,7 @@ +export function FeedbackPage() { + return ( +
+

Feedback

+
+ ) +} 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..eb7dc13 --- /dev/null +++ b/web-client/src/features/helper/api/queries.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query' + +import { helperClient } from './client' + +export const helperKeys = { + report: (memberId: string) => ['helper', 'report', memberId] as const, +} + +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..116ac0c --- /dev/null +++ b/web-client/src/features/helper/pages/HelperPage.tsx @@ -0,0 +1,7 @@ +export function HelperPage() { + return ( +
+

GenAI Helper

+
+ ) +} 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..1c68e4e --- /dev/null +++ b/web-client/src/features/letters/api/queries.ts @@ -0,0 +1,23 @@ +import { useMutation } from '@tanstack/react-query' + +import { lettersClient } from './client' +import type { GeneratePdfRequest, SendMailRequest } from '../types' + +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..325fea2 --- /dev/null +++ b/web-client/src/features/letters/pages/LettersPage.tsx @@ -0,0 +1,7 @@ +export function LettersPage() { + return ( +
+

Letters

+
+ ) +} 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..f5d5e5d --- /dev/null +++ b/web-client/src/features/members/api/queries.ts @@ -0,0 +1,57 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { membersClient } from './client' +import type { Member, MemberCreate, MemberPartialUpdate, MemberSummary } from '../types' + +export const membersKeys = { + all: ['members'] as const, + detail: (id: string) => ['members', id] as const, +} + +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..d867a94 --- /dev/null +++ b/web-client/src/features/members/pages/MembersPage.tsx @@ -0,0 +1,7 @@ +export function MembersPage() { + return ( +
+

Members

+
+ ) +} 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..dc66dcc --- /dev/null +++ b/web-client/src/features/organization/api/queries.ts @@ -0,0 +1,114 @@ +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 = { + 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 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..4dc0a38 --- /dev/null +++ b/web-client/src/features/organization/pages/OrganizationPage.tsx @@ -0,0 +1,7 @@ +export function OrganizationPage() { + return ( +
+

Organization

+
+ ) +} 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/api/client.ts b/web-client/src/features/payments/api/client.ts new file mode 100644 index 0000000..f3c7d00 --- /dev/null +++ b/web-client/src/features/payments/api/client.ts @@ -0,0 +1,3 @@ +import { createApiClient } from '@/lib/keycloak' + +export const paymentsClient = createApiClient('/api/v1/finance') 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..1a003ce --- /dev/null +++ b/web-client/src/features/payments/api/queries.ts @@ -0,0 +1,74 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { paymentsClient } from './client' +import type { Balance, Transaction, TransactionCreate, TransactionPartialUpdate } from '../types' + +export const paymentsKeys = { + 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 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/client.ts b/web-client/src/features/payments/client.ts deleted file mode 100644 index da9ec92..0000000 --- a/web-client/src/features/payments/client.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createApiClient } from '@/lib/keycloak' - -export const paymentsClient = createApiClient('/api/v1/finances') 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..0947c7b --- /dev/null +++ b/web-client/src/features/payments/pages/PaymentsPage.tsx @@ -0,0 +1,7 @@ +export function PaymentsPage() { + return ( +
+

Payments

+
+ ) +} 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..37222bc --- /dev/null +++ b/web-client/src/features/sport-events/api/queries.ts @@ -0,0 +1,57 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { sportEventsClient } from './client' +import type { SportEvent, EventCreate, EventPartialUpdate, EventSummary } from '../types' + +export const sportEventsKeys = { + all: ['sport-events'] as const, + detail: (id: string) => ['sport-events', id] as const, +} + +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..86bea5a --- /dev/null +++ b/web-client/src/features/sport-events/pages/SportEventsPage.tsx @@ -0,0 +1,7 @@ +export function SportEventsPage() { + return ( +
+

Events

+
+ ) +} 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' From c2e8933606c01e1caba0ddb552227512b8a77679 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 5/7] refactor: remove old flat events api and client files --- web-client/src/features/events/api.ts | 6 ------ web-client/src/features/events/client.ts | 3 --- 2 files changed, 9 deletions(-) delete mode 100644 web-client/src/features/events/api.ts delete mode 100644 web-client/src/features/events/client.ts 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') From accc1dda4b85e3a934734ac7aecc270487c24294 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:53:33 +0200 Subject: [PATCH 6/7] feat: display hello messages from all backend services on feature pages --- web-client/src/features/feedback/api/queries.ts | 8 ++++++++ .../src/features/feedback/pages/FeedbackPage.tsx | 5 +++++ web-client/src/features/helper/api/queries.ts | 11 +++++++++++ web-client/src/features/helper/pages/HelperPage.tsx | 5 +++++ web-client/src/features/letters/api/queries.ts | 13 ++++++++++++- .../src/features/letters/pages/LettersPage.tsx | 5 +++++ web-client/src/features/members/api/queries.ts | 8 ++++++++ .../src/features/members/pages/MembersPage.tsx | 5 +++++ web-client/src/features/organization/api/queries.ts | 8 ++++++++ .../organization/pages/OrganizationPage.tsx | 5 +++++ web-client/src/features/payments/api/queries.ts | 8 ++++++++ .../src/features/payments/pages/PaymentsPage.tsx | 5 +++++ web-client/src/features/sport-events/api/queries.ts | 8 ++++++++ .../features/sport-events/pages/SportEventsPage.tsx | 5 +++++ 14 files changed, 98 insertions(+), 1 deletion(-) diff --git a/web-client/src/features/feedback/api/queries.ts b/web-client/src/features/feedback/api/queries.ts index 4e5e67e..d53af6d 100644 --- a/web-client/src/features/feedback/api/queries.ts +++ b/web-client/src/features/feedback/api/queries.ts @@ -6,6 +6,14 @@ import type { Feedback, FeedbackCreate, FeedbackPartialUpdate, FeedbackSummary } 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() { diff --git a/web-client/src/features/feedback/pages/FeedbackPage.tsx b/web-client/src/features/feedback/pages/FeedbackPage.tsx index 7de50c3..9ac09d2 100644 --- a/web-client/src/features/feedback/pages/FeedbackPage.tsx +++ b/web-client/src/features/feedback/pages/FeedbackPage.tsx @@ -1,7 +1,12 @@ +import { useFeedbackHello } from '../api' + export function FeedbackPage() { + const { data: hello } = useFeedbackHello() + return (

Feedback

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/helper/api/queries.ts b/web-client/src/features/helper/api/queries.ts index eb7dc13..434334a 100644 --- a/web-client/src/features/helper/api/queries.ts +++ b/web-client/src/features/helper/api/queries.ts @@ -3,9 +3,20 @@ 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.replace(/<[^>]+>/g, '').trim() + ), + }) +} + export function useMemberReport(memberId: string) { return useQuery({ queryKey: helperKeys.report(memberId), diff --git a/web-client/src/features/helper/pages/HelperPage.tsx b/web-client/src/features/helper/pages/HelperPage.tsx index 116ac0c..3fd9cc3 100644 --- a/web-client/src/features/helper/pages/HelperPage.tsx +++ b/web-client/src/features/helper/pages/HelperPage.tsx @@ -1,7 +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/letters/api/queries.ts b/web-client/src/features/letters/api/queries.ts index 1c68e4e..6701028 100644 --- a/web-client/src/features/letters/api/queries.ts +++ b/web-client/src/features/letters/api/queries.ts @@ -1,8 +1,19 @@ -import { useMutation } from '@tanstack/react-query' +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 => diff --git a/web-client/src/features/letters/pages/LettersPage.tsx b/web-client/src/features/letters/pages/LettersPage.tsx index 325fea2..f2fb8e3 100644 --- a/web-client/src/features/letters/pages/LettersPage.tsx +++ b/web-client/src/features/letters/pages/LettersPage.tsx @@ -1,7 +1,12 @@ +import { useLettersHello } from '../api' + export function LettersPage() { + const { data: hello } = useLettersHello() + return (

Letters

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/members/api/queries.ts b/web-client/src/features/members/api/queries.ts index f5d5e5d..7a1d1d7 100644 --- a/web-client/src/features/members/api/queries.ts +++ b/web-client/src/features/members/api/queries.ts @@ -4,10 +4,18 @@ 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, diff --git a/web-client/src/features/members/pages/MembersPage.tsx b/web-client/src/features/members/pages/MembersPage.tsx index d867a94..c12ffd7 100644 --- a/web-client/src/features/members/pages/MembersPage.tsx +++ b/web-client/src/features/members/pages/MembersPage.tsx @@ -1,7 +1,12 @@ +import { useMembersHello } from '../api' + export function MembersPage() { + const { data: hello } = useMembersHello() + return (

Members

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/organization/api/queries.ts b/web-client/src/features/organization/api/queries.ts index dc66dcc..cbb4378 100644 --- a/web-client/src/features/organization/api/queries.ts +++ b/web-client/src/features/organization/api/queries.ts @@ -11,12 +11,20 @@ import type { } 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, diff --git a/web-client/src/features/organization/pages/OrganizationPage.tsx b/web-client/src/features/organization/pages/OrganizationPage.tsx index 4dc0a38..0b180d8 100644 --- a/web-client/src/features/organization/pages/OrganizationPage.tsx +++ b/web-client/src/features/organization/pages/OrganizationPage.tsx @@ -1,7 +1,12 @@ +import { useOrganizationHello } from '../api' + export function OrganizationPage() { + const { data: hello } = useOrganizationHello() + return (

Organization

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/payments/api/queries.ts b/web-client/src/features/payments/api/queries.ts index 1a003ce..0ef75cf 100644 --- a/web-client/src/features/payments/api/queries.ts +++ b/web-client/src/features/payments/api/queries.ts @@ -4,12 +4,20 @@ 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, diff --git a/web-client/src/features/payments/pages/PaymentsPage.tsx b/web-client/src/features/payments/pages/PaymentsPage.tsx index 0947c7b..9ca646b 100644 --- a/web-client/src/features/payments/pages/PaymentsPage.tsx +++ b/web-client/src/features/payments/pages/PaymentsPage.tsx @@ -1,7 +1,12 @@ +import { usePaymentsHello } from '../api' + export function PaymentsPage() { + const { data: hello } = usePaymentsHello() + return (

Payments

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/sport-events/api/queries.ts b/web-client/src/features/sport-events/api/queries.ts index 37222bc..97794bc 100644 --- a/web-client/src/features/sport-events/api/queries.ts +++ b/web-client/src/features/sport-events/api/queries.ts @@ -4,10 +4,18 @@ 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, diff --git a/web-client/src/features/sport-events/pages/SportEventsPage.tsx b/web-client/src/features/sport-events/pages/SportEventsPage.tsx index 86bea5a..52b70cf 100644 --- a/web-client/src/features/sport-events/pages/SportEventsPage.tsx +++ b/web-client/src/features/sport-events/pages/SportEventsPage.tsx @@ -1,7 +1,12 @@ +import { useSportEventsHello } from '../api' + export function SportEventsPage() { + const { data: hello } = useSportEventsHello() + return (

Events

+ {hello &&

{hello}

}
) } From d8885e40f2ed6279bc5c791c37b4070d2acdaf21 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:05:14 +0200 Subject: [PATCH 7/7] fix: simplify hello message retrieval by removing HTML stripping --- web-client/src/features/helper/api/queries.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web-client/src/features/helper/api/queries.ts b/web-client/src/features/helper/api/queries.ts index 434334a..68abbd3 100644 --- a/web-client/src/features/helper/api/queries.ts +++ b/web-client/src/features/helper/api/queries.ts @@ -10,10 +10,7 @@ export const helperKeys = { export function useHelperHello() { return useQuery({ queryKey: helperKeys.hello, - queryFn: () => - helperClient.get('/hello').then(r => - r.data.replace(/<[^>]+>/g, '').trim() - ), + queryFn: () => helperClient.get('/hello').then(r => r.data), }) }