From bdf863fe14e5585928a88692f88ab22beefe50b7 Mon Sep 17 00:00:00 2001 From: survikrowa Date: Sun, 25 Jan 2026 17:04:06 +0100 Subject: [PATCH 1/8] feat: integrate Gluestack UI components and update project configuration --- .github/copilot-instructions.md | 2 + .npmrc | 1 + apps/native/.nvmrc | 2 +- apps/native/.watchmanconfig | 10 + apps/native/app.json | 21 +- apps/native/app/_layout.tsx | 44 +- apps/native/babel.config.js | 17 +- .../ui/gluestack-ui-provider/config.ts | 309 + .../ui/gluestack-ui-provider/index.tsx | 39 + apps/native/global.css | 3 + apps/native/metro.config.js | 9 +- .../games_status_form/games_status_form.tsx | 37 +- .../use_games_status_form.ts | 2 +- .../use_upsert_game_status.ts | 2 +- apps/native/modules/graphql/apollo_client.ts | 2 +- .../layouts/go_back_header/go_back_header.tsx | 19 +- apps/native/modules/layouts/header/header.tsx | 32 +- .../layouts/header/search/search_button.tsx | 2 +- .../layouts/header/user_profile_button.tsx | 2 +- .../modules/screens/auth/auth_screen.tsx | 17 +- apps/native/modules/screens/auth/use_auth.ts | 2 +- .../use_collection_add_form.ts | 2 +- .../collection_details/collection_details.tsx | 24 +- .../collection_details_empty.tsx | 8 +- .../collection_details_fab.tsx | 23 +- .../new_collection_form.tsx | 13 +- .../use_new_collection_form.ts | 2 +- .../new_collection/new_collection_page.tsx | 8 +- .../friends_list_fab/friends_list_fab.tsx | 26 +- .../friends_list/friends_list_screen.tsx | 29 +- .../friends_requests_empty.tsx | 12 +- .../friends_requests_results.tsx | 10 +- .../friends_requests_sender.tsx | 28 +- .../use_accept_friend_request.ts | 2 +- .../friends_search_empty.tsx | 7 +- .../friends_search_result.tsx | 16 +- ...ends_search_result_send_request_button.tsx | 8 +- .../friends_search_results.tsx | 8 +- .../use_send_friend_request.ts | 2 +- .../friends_search/friends_search_screen.tsx | 12 +- .../user_profile_info_card.tsx | 13 +- .../user_profile/user_profile_screen.tsx | 13 +- .../screens/game/game_info/game_info.tsx | 8 +- .../game_preparing_info.tsx | 8 +- .../modules/screens/game/game_screen.tsx | 15 +- .../screens/game/game_tabs/game_tabs.tsx | 14 +- .../game_status_reviews_screen.tsx | 30 +- .../modules/screens/games/games_screen.tsx | 13 +- .../games_status_add_form_screen.tsx | 8 +- .../games_status_categories_fab.tsx | 26 +- .../games_status_edit_form.tsx | 12 +- .../games_status_filters_modal.tsx | 20 +- .../games_status_filters_modal_section.tsx | 11 +- .../games_status_list/games_status_list.tsx | 30 +- .../games_status_list_item.tsx | 24 +- .../use_remove_game_status.ts | 4 +- .../friends_activity/friends_activity.tsx | 7 +- .../modules/screens/homepage/home_screen.tsx | 20 +- .../incoming_games_carousel.tsx | 11 +- .../photo_editor/photo_editor.tsx | 2 +- .../profile_editor_form.tsx | 8 +- .../use_profile_editor_form.ts | 2 +- .../screens/profile/profile_screen.tsx | 29 +- .../hltb_document_picker.tsx | 10 +- .../use_user_profile_info.ts | 4 +- .../search/search_input/search_input.tsx | 15 +- .../search/search_results/search_results.tsx | 8 +- .../modules/screens/search/search_screen.tsx | 12 +- .../user_game_status_friends_reviews.tsx | 11 +- .../user_game_status_screen.tsx | 7 +- ..._status_achievements_completed_section.tsx | 8 +- .../user_game_status_completed_in_section.tsx | 8 +- ...er_game_status_game_completion_section.tsx | 4 +- .../user_game_status_main_section.tsx | 7 +- .../user_game_status_platform_section.tsx | 9 +- .../user_game_status_review_section.tsx | 4 +- .../user_game_status_score_section.tsx | 7 +- .../screens/user_stats/user_stats_screen.tsx | 34 +- .../user_activity_card/user_activity_card.tsx | 23 +- apps/native/nativewind-env.d.ts | 1 + apps/native/package.json | 36 +- apps/native/tailwind.config.js | 208 + apps/native/tsconfig.json | 9 +- .../ui/data-display/bar-chart/bar-chart.tsx | 183 + apps/native/ui/data-display/carousel.tsx | 142 +- apps/native/ui/feedback/skeleton/skeleton.tsx | 160 + apps/native/ui/feedback/toast/toast.tsx | 270 +- .../feedback/toast/use_toast_controller.tsx | 32 + apps/native/ui/forms/checkbox.tsx | 8 +- apps/native/ui/forms/pressable/pressable.tsx | 36 + apps/native/ui/layout/box/box.tsx | 19 + apps/native/ui/layout/box/styles.tsx | 9 + apps/native/ui/layout/hstack/hstack.tsx | 28 + apps/native/ui/layout/hstack/styles.tsx | 25 + apps/native/ui/layout/vstack/styles.tsx | 25 + apps/native/ui/layout/vstack/vstack.tsx | 28 + apps/native/ui/media_and_icons/icon/icon.tsx | 1587 ++++++ apps/native/ui/overlay/alert_dialog.tsx | 18 +- apps/native/ui/overlay/fab/fab.tsx | 155 + apps/native/ui/typography/styles.tsx | 47 + apps/native/ui/typography/text.tsx | 47 +- yarn.lock | 5046 +++++++++++++---- 102 files changed, 7800 insertions(+), 1632 deletions(-) create mode 100644 .npmrc create mode 100644 apps/native/.watchmanconfig create mode 100644 apps/native/components/ui/gluestack-ui-provider/config.ts create mode 100644 apps/native/components/ui/gluestack-ui-provider/index.tsx create mode 100644 apps/native/global.css create mode 100644 apps/native/nativewind-env.d.ts create mode 100644 apps/native/tailwind.config.js create mode 100644 apps/native/ui/data-display/bar-chart/bar-chart.tsx create mode 100644 apps/native/ui/feedback/skeleton/skeleton.tsx create mode 100644 apps/native/ui/feedback/toast/use_toast_controller.tsx create mode 100644 apps/native/ui/forms/pressable/pressable.tsx create mode 100644 apps/native/ui/layout/box/box.tsx create mode 100644 apps/native/ui/layout/box/styles.tsx create mode 100644 apps/native/ui/layout/hstack/hstack.tsx create mode 100644 apps/native/ui/layout/hstack/styles.tsx create mode 100644 apps/native/ui/layout/vstack/styles.tsx create mode 100644 apps/native/ui/layout/vstack/vstack.tsx create mode 100644 apps/native/ui/media_and_icons/icon/icon.tsx create mode 100644 apps/native/ui/overlay/fab/fab.tsx create mode 100644 apps/native/ui/typography/styles.tsx diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6c77982..45f8a21 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -347,6 +347,7 @@ src/ 9. Use debouncing for search inputs (use-debounce) 10. Handle loading and error states consistently 11. **Native UI Development**: When the "use-gluestack-components" MCP server is connected and you're working on React Native UI, always consult it first for component selection and implementation patterns. Prefer Gluestack UI components for accessibility and cross-platform consistency. +12. **NEVER use barrel exports** (index.ts/index.js files that re-export from other files). Always import directly from the source file (e.g., `import { Box } from "../layout/box/box"` instead of `import { Box } from "../layout/box"`). Barrel exports cause performance issues, circular dependency problems, and make it harder to track imports. ## Useful Commands ```bash @@ -393,6 +394,7 @@ yarn build-android # EAS build Android - **API**: Uses tsconfig paths (e.g., `@/modules/...`) - **Web**: `@/` → `src/` - **Native**: Relative imports for modules, absolute for ui +- **Important**: Always import directly from source files, NOT from index.ts barrel exports (e.g., use `from "./box/box"` not `from "./box"`) ## Dependencies Management - Use exact versions for critical dependencies diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..cc8df9d --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted \ No newline at end of file diff --git a/apps/native/.nvmrc b/apps/native/.nvmrc index adb5558..cf2efde 100644 --- a/apps/native/.nvmrc +++ b/apps/native/.nvmrc @@ -1 +1 @@ -22.14.0 \ No newline at end of file +24.13.0 \ No newline at end of file diff --git a/apps/native/.watchmanconfig b/apps/native/.watchmanconfig new file mode 100644 index 0000000..ea24c12 --- /dev/null +++ b/apps/native/.watchmanconfig @@ -0,0 +1,10 @@ +{ + "ignore_dirs": [ + "node_modules", + ".git", + "android", + "ios", + "build", + ".expo" + ] +} diff --git a/apps/native/app.json b/apps/native/app.json index 9550810..0bb0692 100644 --- a/apps/native/app.json +++ b/apps/native/app.json @@ -37,6 +37,24 @@ ] }, "plugins": [ + [ + "expo-build-properties", + { + "android": { + "newArchEnabled": true + }, + "ios": { + "newArchEnabled": true + } + } + ], + [ + "expo-dev-client", + { + "launchMode": "most-recent", + "addGeneratedScheme": false + } + ], "expo-router", [ "react-native-auth0", @@ -51,7 +69,8 @@ } ], "expo-font", - "expo-secure-store" + "expo-secure-store", + "expo-asset" ], "extra": { "router": { diff --git a/apps/native/app/_layout.tsx b/apps/native/app/_layout.tsx index c948d6e..7f7b917 100644 --- a/apps/native/app/_layout.tsx +++ b/apps/native/app/_layout.tsx @@ -1,19 +1,24 @@ -import { ToastProvider } from "@tamagui/toast"; import { useFonts } from "expo-font"; import { SplashScreen, Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { useCallback } from "react"; import { LogBox } from "react-native"; import { Auth0Provider } from "react-native-auth0"; -import { SafeAreaProvider } from "react-native-safe-area-context"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { + initialWindowMetrics, + SafeAreaProvider, +} from "react-native-safe-area-context"; import { TamaguiProvider } from "tamagui"; -import { Toast } from "ui/feedback/toast/toast"; -import { ApolloProvider } from "../modules/graphql/apollo_provider"; -import { Header } from "../modules/layouts/header/header"; -import { SafeToastViewport } from "../modules/layouts/safe_toast_viewport/safe_toast_viewport"; import tamaguiConfig from "../tamagui.config"; +import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider"; +import { ApolloProvider } from "@/modules/graphql/apollo_provider"; +import { Header } from "@/modules/layouts/header/header"; + +import "@/global.css"; + SplashScreen.preventAutoHideAsync(); const AUTH0_DOMAIN = process.env.EXPO_PUBLIC_AUTH0_DOMAIN; @@ -36,14 +41,15 @@ const RootLayout = () => { return null; } return ( - - - - - - - - + + + + + + { options={{ headerShown: true, header: Header }} /> - - - - - + + + + + ); }; diff --git a/apps/native/babel.config.js b/apps/native/babel.config.js index 3124469..b22c1ca 100644 --- a/apps/native/babel.config.js +++ b/apps/native/babel.config.js @@ -1,19 +1,28 @@ module.exports = function (api) { api.cache(true); + return { - presets: ["babel-preset-expo"], + presets: [ + ["babel-preset-expo", { jsxImportSource: "nativewind" }], + "nativewind/babel", + ], + plugins: [ - "expo-router/babel", - "react-native-reanimated/plugin", [ "module-resolver", { + root: ["./"], alias: { ui: "./ui", + "@": "./", + "tailwind.config": "./tailwind.config.js", }, extensions: [".js", ".jsx", ".ts", ".tsx"], }, ], - ], + "react-native-worklets/plugin", + // Explicitly enable React Refresh for Fast Refresh + process.env.NODE_ENV !== "production" && "react-refresh/babel", + ].filter(Boolean), }; }; diff --git a/apps/native/components/ui/gluestack-ui-provider/config.ts b/apps/native/components/ui/gluestack-ui-provider/config.ts new file mode 100644 index 0000000..32842d9 --- /dev/null +++ b/apps/native/components/ui/gluestack-ui-provider/config.ts @@ -0,0 +1,309 @@ +"use client"; +import { vars } from "nativewind"; + +export const config = { + light: vars({ + "--color-primary-0": "179 179 179", + "--color-primary-50": "153 153 153", + "--color-primary-100": "128 128 128", + "--color-primary-200": "115 115 115", + "--color-primary-300": "102 102 102", + "--color-primary-400": "82 82 82", + "--color-primary-500": "51 51 51", + "--color-primary-600": "41 41 41", + "--color-primary-700": "31 31 31", + "--color-primary-800": "13 13 13", + "--color-primary-900": "10 10 10", + "--color-primary-950": "8 8 8", + + /* Secondary */ + "--color-secondary-0": "253 253 253", + "--color-secondary-50": "251 251 251", + "--color-secondary-100": "246 246 246", + "--color-secondary-200": "242 242 242", + "--color-secondary-300": "237 237 237", + "--color-secondary-400": "230 230 231", + "--color-secondary-500": "217 217 219", + "--color-secondary-600": "198 199 199", + "--color-secondary-700": "189 189 189", + "--color-secondary-800": "177 177 177", + "--color-secondary-900": "165 164 164", + "--color-secondary-950": "157 157 157", + + /* Tertiary */ + "--color-tertiary-0": "255 250 245", + "--color-tertiary-50": "255 242 229", + "--color-tertiary-100": "255 233 213", + "--color-tertiary-200": "254 209 170", + "--color-tertiary-300": "253 180 116", + "--color-tertiary-400": "251 157 75", + "--color-tertiary-500": "231 129 40", + "--color-tertiary-600": "215 117 31", + "--color-tertiary-700": "180 98 26", + "--color-tertiary-800": "130 73 23", + "--color-tertiary-900": "108 61 19", + "--color-tertiary-950": "84 49 18", + + /* Error */ + "--color-error-0": "254 233 233", + "--color-error-50": "254 226 226", + "--color-error-100": "254 202 202", + "--color-error-200": "252 165 165", + "--color-error-300": "248 113 113", + "--color-error-400": "239 68 68", + "--color-error-500": "230 53 53", + "--color-error-600": "220 38 38", + "--color-error-700": "185 28 28", + "--color-error-800": "153 27 27", + "--color-error-900": "127 29 29", + "--color-error-950": "83 19 19", + + /* Success */ + "--color-success-0": "228 255 244", + "--color-success-50": "202 255 232", + "--color-success-100": "162 241 192", + "--color-success-200": "132 211 162", + "--color-success-300": "102 181 132", + "--color-success-400": "72 151 102", + "--color-success-500": "52 131 82", + "--color-success-600": "42 121 72", + "--color-success-700": "32 111 62", + "--color-success-800": "22 101 52", + "--color-success-900": "20 83 45", + "--color-success-950": "27 50 36", + + /* Warning */ + "--color-warning-0": "255 249 245", + "--color-warning-50": "255 244 236", + "--color-warning-100": "255 231 213", + "--color-warning-200": "254 205 170", + "--color-warning-300": "253 173 116", + "--color-warning-400": "251 149 75", + "--color-warning-500": "231 120 40", + "--color-warning-600": "215 108 31", + "--color-warning-700": "180 90 26", + "--color-warning-800": "130 68 23", + "--color-warning-900": "108 56 19", + "--color-warning-950": "84 45 18", + + /* Info */ + "--color-info-0": "236 248 254", + "--color-info-50": "199 235 252", + "--color-info-100": "162 221 250", + "--color-info-200": "124 207 248", + "--color-info-300": "87 194 246", + "--color-info-400": "50 180 244", + "--color-info-500": "13 166 242", + "--color-info-600": "11 141 205", + "--color-info-700": "9 115 168", + "--color-info-800": "7 90 131", + "--color-info-900": "5 64 93", + "--color-info-950": "3 38 56", + + /* Typography */ + "--color-typography-0": "254 254 255", + "--color-typography-50": "245 245 245", + "--color-typography-100": "229 229 229", + "--color-typography-200": "219 219 220", + "--color-typography-300": "212 212 212", + "--color-typography-400": "163 163 163", + "--color-typography-500": "140 140 140", + "--color-typography-600": "115 115 115", + "--color-typography-700": "82 82 82", + "--color-typography-800": "64 64 64", + "--color-typography-900": "38 38 39", + "--color-typography-950": "23 23 23", + + /* Outline */ + "--color-outline-0": "253 254 254", + "--color-outline-50": "243 243 243", + "--color-outline-100": "230 230 230", + "--color-outline-200": "221 220 219", + "--color-outline-300": "211 211 211", + "--color-outline-400": "165 163 163", + "--color-outline-500": "140 141 141", + "--color-outline-600": "115 116 116", + "--color-outline-700": "83 82 82", + "--color-outline-800": "65 65 65", + "--color-outline-900": "39 38 36", + "--color-outline-950": "26 23 23", + + /* Background */ + "--color-background-0": "255 255 255", + "--color-background-50": "246 246 246", + "--color-background-100": "242 241 241", + "--color-background-200": "220 219 219", + "--color-background-300": "213 212 212", + "--color-background-400": "162 163 163", + "--color-background-500": "142 142 142", + "--color-background-600": "116 116 116", + "--color-background-700": "83 82 82", + "--color-background-800": "65 64 64", + "--color-background-900": "39 38 37", + "--color-background-950": "18 18 18", + + /* Background Special */ + "--color-background-error": "254 241 241", + "--color-background-warning": "255 243 234", + "--color-background-success": "237 252 242", + "--color-background-muted": "247 248 247", + "--color-background-info": "235 248 254", + + /* Focus Ring Indicator */ + "--color-indicator-primary": "55 55 55", + "--color-indicator-info": "83 153 236", + "--color-indicator-error": "185 28 28", + }), + dark: vars({ + "--color-primary-0": "166 166 166", + "--color-primary-50": "175 175 175", + "--color-primary-100": "186 186 186", + "--color-primary-200": "197 197 197", + "--color-primary-300": "212 212 212", + "--color-primary-400": "221 221 221", + "--color-primary-500": "230 230 230", + "--color-primary-600": "240 240 240", + "--color-primary-700": "250 250 250", + "--color-primary-800": "253 253 253", + "--color-primary-900": "254 249 249", + "--color-primary-950": "253 252 252", + + /* Secondary */ + "--color-secondary-0": "20 20 20", + "--color-secondary-50": "23 23 23", + "--color-secondary-100": "31 31 31", + "--color-secondary-200": "39 39 39", + "--color-secondary-300": "44 44 44", + "--color-secondary-400": "56 57 57", + "--color-secondary-500": "63 64 64", + "--color-secondary-600": "86 86 86", + "--color-secondary-700": "110 110 110", + "--color-secondary-800": "135 135 135", + "--color-secondary-900": "150 150 150", + "--color-secondary-950": "164 164 164", + + /* Tertiary */ + "--color-tertiary-0": "84 49 18", + "--color-tertiary-50": "108 61 19", + "--color-tertiary-100": "130 73 23", + "--color-tertiary-200": "180 98 26", + "--color-tertiary-300": "215 117 31", + "--color-tertiary-400": "231 129 40", + "--color-tertiary-500": "251 157 75", + "--color-tertiary-600": "253 180 116", + "--color-tertiary-700": "254 209 170", + "--color-tertiary-800": "255 233 213", + "--color-tertiary-900": "255 242 229", + "--color-tertiary-950": "255 250 245", + + /* Error */ + "--color-error-0": "83 19 19", + "--color-error-50": "127 29 29", + "--color-error-100": "153 27 27", + "--color-error-200": "185 28 28", + "--color-error-300": "220 38 38", + "--color-error-400": "230 53 53", + "--color-error-500": "239 68 68", + "--color-error-600": "249 97 96", + "--color-error-700": "229 91 90", + "--color-error-800": "254 202 202", + "--color-error-900": "254 226 226", + "--color-error-950": "254 233 233", + + /* Success */ + "--color-success-0": "27 50 36", + "--color-success-50": "20 83 45", + "--color-success-100": "22 101 52", + "--color-success-200": "32 111 62", + "--color-success-300": "42 121 72", + "--color-success-400": "52 131 82", + "--color-success-500": "72 151 102", + "--color-success-600": "102 181 132", + "--color-success-700": "132 211 162", + "--color-success-800": "162 241 192", + "--color-success-900": "202 255 232", + "--color-success-950": "228 255 244", + + /* Warning */ + "--color-warning-0": "84 45 18", + "--color-warning-50": "108 56 19", + "--color-warning-100": "130 68 23", + "--color-warning-200": "180 90 26", + "--color-warning-300": "215 108 31", + "--color-warning-400": "231 120 40", + "--color-warning-500": "251 149 75", + "--color-warning-600": "253 173 116", + "--color-warning-700": "254 205 170", + "--color-warning-800": "255 231 213", + "--color-warning-900": "255 244 237", + "--color-warning-950": "255 249 245", + + /* Info */ + "--color-info-0": "3 38 56", + "--color-info-50": "5 64 93", + "--color-info-100": "7 90 131", + "--color-info-200": "9 115 168", + "--color-info-300": "11 141 205", + "--color-info-400": "13 166 242", + "--color-info-500": "50 180 244", + "--color-info-600": "87 194 246", + "--color-info-700": "124 207 248", + "--color-info-800": "162 221 250", + "--color-info-900": "199 235 252", + "--color-info-950": "236 248 254", + + /* Typography */ + "--color-typography-0": "23 23 23", + "--color-typography-50": "38 38 39", + "--color-typography-100": "64 64 64", + "--color-typography-200": "82 82 82", + "--color-typography-300": "115 115 115", + "--color-typography-400": "140 140 140", + "--color-typography-500": "163 163 163", + "--color-typography-600": "212 212 212", + "--color-typography-700": "219 219 220", + "--color-typography-800": "229 229 229", + "--color-typography-900": "245 245 245", + "--color-typography-950": "254 254 255", + + /* Outline */ + "--color-outline-0": "26 23 23", + "--color-outline-50": "39 38 36", + "--color-outline-100": "65 65 65", + "--color-outline-200": "83 82 82", + "--color-outline-300": "115 116 116", + "--color-outline-400": "140 141 141", + "--color-outline-500": "165 163 163", + "--color-outline-600": "211 211 211", + "--color-outline-700": "221 220 219", + "--color-outline-800": "230 230 230", + "--color-outline-900": "243 243 243", + "--color-outline-950": "253 254 254", + + /* Background */ + "--color-background-0": "18 18 18", + "--color-background-50": "39 38 37", + "--color-background-100": "65 64 64", + "--color-background-200": "83 82 82", + "--color-background-300": "116 116 116", + "--color-background-400": "142 142 142", + "--color-background-500": "162 163 163", + "--color-background-600": "213 212 212", + "--color-background-700": "229 228 228", + "--color-background-800": "242 241 241", + "--color-background-900": "246 246 246", + "--color-background-950": "255 255 255", + + /* Background Special */ + "--color-background-error": "66 43 43", + "--color-background-warning": "65 47 35", + "--color-background-success": "28 43 33", + "--color-background-muted": "51 51 51", + "--color-background-info": "26 40 46", + + /* Focus Ring Indicator */ + "--color-indicator-primary": "247 247 247", + "--color-indicator-info": "161 199 245", + "--color-indicator-error": "232 70 69", + }), +}; diff --git a/apps/native/components/ui/gluestack-ui-provider/index.tsx b/apps/native/components/ui/gluestack-ui-provider/index.tsx new file mode 100644 index 0000000..311fefb --- /dev/null +++ b/apps/native/components/ui/gluestack-ui-provider/index.tsx @@ -0,0 +1,39 @@ +import { OverlayProvider } from "@gluestack-ui/core/overlay/creator"; +import { ToastProvider } from "@gluestack-ui/core/toast/creator"; +import { useColorScheme } from "nativewind"; +import React, { useEffect } from "react"; +import { View, ViewProps } from "react-native"; + +import { config } from "./config"; + +export type ModeType = "light" | "dark" | "system"; + +export function GluestackUIProvider({ + mode = "light", + ...props +}: { + mode?: ModeType; + children?: React.ReactNode; + style?: ViewProps["style"]; +}) { + const { colorScheme, setColorScheme } = useColorScheme(); + + useEffect(() => { + setColorScheme(mode); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode]); + + return ( + + + {props.children} + + + ); +} diff --git a/apps/native/global.css b/apps/native/global.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/native/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/native/metro.config.js b/apps/native/metro.config.js index a980779..d2a3e84 100644 --- a/apps/native/metro.config.js +++ b/apps/native/metro.config.js @@ -1,8 +1,9 @@ // Learn more https://docs.expo.io/guides/customizing-metro const { getDefaultConfig } = require("expo/metro-config"); +const { withNativeWind } = require("nativewind/metro"); const path = require("path"); -module.exports = (() => { +const getModifiedConfig = () => { // Find the workspace root, this can be replaced with `find-yarn-workspace-root` const workspaceRoot = path.resolve(__dirname, "../../"); const projectRoot = __dirname; @@ -32,4 +33,8 @@ module.exports = (() => { }; return config; -})(); +}; + +const modifiedConfig = getModifiedConfig(); + +module.exports = withNativeWind(modifiedConfig, { input: "./global.css" }); diff --git a/apps/native/modules/games_status/games_status_form/games_status_form.tsx b/apps/native/modules/games_status/games_status_form/games_status_form.tsx index 09a111b..4a9664f 100644 --- a/apps/native/modules/games_status/games_status_form/games_status_form.tsx +++ b/apps/native/modules/games_status/games_status_form/games_status_form.tsx @@ -1,6 +1,6 @@ import { ErrorMessage } from "@hookform/error-message"; import { Controller, FormProvider } from "react-hook-form"; -import { Button, Form, Separator, View, XStack, YStack } from "tamagui"; +import { Button, Form, Separator, View } from "tamagui"; import { Checkbox } from "ui/forms/checkbox"; import { Input } from "ui/forms/input"; import { Select } from "ui/forms/select"; @@ -16,6 +16,9 @@ import { } from "./use_games_status_form"; import { GameInfoQuery } from "../../screens/game/use_get_game_info/game_info.generated"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type GamesStatusFormProps = { initialValues?: InitialValues; gameStatusId?: number; @@ -38,7 +41,7 @@ export const GamesStatusForm = ({
- + Status* @@ -73,13 +76,15 @@ export const GamesStatusForm = ({ name="status" control={control} /> - + Czas gry - + - + - + Platforma* @@ -181,9 +186,9 @@ export const GamesStatusForm = ({ name="platform" control={control} /> - + - + Osiągnięcia @@ -210,13 +215,19 @@ export const GamesStatusForm = ({ Ta gra lub platforma nie posiada osiągnięć )} - + - + Ocena - + - + - + - - - + + + ); }; diff --git a/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/use_accept_friend_request/use_accept_friend_request.ts b/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/use_accept_friend_request/use_accept_friend_request.ts index 20c68c1..9b2bc58 100644 --- a/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/use_accept_friend_request/use_accept_friend_request.ts +++ b/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/use_accept_friend_request/use_accept_friend_request.ts @@ -1,4 +1,4 @@ -import { useToastController } from "@tamagui/toast"; +import { useToastController } from "ui/feedback/toast/use_toast_controller"; import { useAcceptFriendRequestMutation } from "./accept_friend_request_mutation.generated"; diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_empty.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_empty.tsx index 8dcda84..c234229 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_empty.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_empty.tsx @@ -1,12 +1,13 @@ -import { YStack } from "tamagui"; import { Text } from "ui/typography/text"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const FriendsSearchEmpty = () => { return ( - + Brak wyników - + ); }; diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result.tsx index 8ce8e44..a34bb69 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result.tsx @@ -1,11 +1,13 @@ -import { Separator, XStack } from "tamagui"; +import { Separator } from "tamagui"; import { Text } from "ui/typography/text"; import { FriendsSearchResultSendRequestButton } from "./friends_search_result_send_request_button/friends_search_result_send_request_button"; -import { truncateString } from "../../../../../strings/truncate_string"; -import { UserAvatar } from "../../../../../user/user_avatar/user_avatar"; import { useSendFriendRequest } from "../use_send_friend_request/use_send_friend_request"; +import { truncateString } from "@/modules/strings/truncate_string"; +import { UserAvatar } from "@/modules/user/user_avatar/user_avatar"; +import { HStack } from "@/ui/layout/hstack/hstack"; + type FriendsSearchResultProps = { oauthId: string; avatarUrl?: string; @@ -34,13 +36,13 @@ export const FriendsSearchResult = ({ return ( <> - - + + {truncateString(name || "", 20)} - + - + {usersLength > 1 && usersLength - 1 !== currentIndex && ( )} diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result_send_request_button/friends_search_result_send_request_button.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result_send_request_button/friends_search_result_send_request_button.tsx index 481639c..42cf218 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result_send_request_button/friends_search_result_send_request_button.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result_send_request_button/friends_search_result_send_request_button.tsx @@ -1,6 +1,8 @@ -import { Button, Spinner, YStack } from "tamagui"; +import { Button, Spinner } from "tamagui"; import { Text } from "ui/typography/text"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type FriendsSearchResultSendRequestButtonProps = { isFriendRequestSent: boolean; handleSendFriendRequest: (oauthId: string) => Promise; @@ -18,14 +20,14 @@ export const FriendsSearchResultSendRequestButton = ({ }: FriendsSearchResultSendRequestButtonProps) => { if (isFriendRequestSent) { return ( - + Oczekuje na akceptacje - + ); } diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_results.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_results.tsx index 8527fdd..dd181c7 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_results.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_results.tsx @@ -1,8 +1,10 @@ -import { ScrollView, YStack } from "tamagui"; +import { ScrollView } from "tamagui"; import { FriendsSearchEmpty } from "./friends_search_empty"; import { FriendsSearchResult } from "./friends_search_result/friends_search_result"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type FriendsSearchResultsProps = { users: User[]; }; @@ -20,7 +22,7 @@ export const FriendsSearchResults = ({ users }: FriendsSearchResultsProps) => { } return ( - + {users.map((user, index) => ( { isFriendRequestSent={user.isFriendRequestSent} /> ))} - + ); }; diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/use_send_friend_request/use_send_friend_request.ts b/apps/native/modules/screens/friends/friends_search/friends_search_results/use_send_friend_request/use_send_friend_request.ts index 4579102..3cf2b91 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/use_send_friend_request/use_send_friend_request.ts +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/use_send_friend_request/use_send_friend_request.ts @@ -1,4 +1,4 @@ -import { useToastController } from "@tamagui/toast"; +import { useToastController } from "ui/feedback/toast/use_toast_controller"; import { useSendFriendRequestMutation } from "./send_friend_request_mutation.generated"; diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_screen.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_screen.tsx index c66698c..4f0875d 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_screen.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_screen.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Spinner, YStack } from "tamagui"; +import { Spinner } from "tamagui"; import { Text } from "ui/typography/text"; import { useDebounce } from "use-debounce"; @@ -7,12 +7,14 @@ import { FriendsSearchResults } from "./friends_search_results/friends_search_re import { useGetUsersSearch } from "./use_get_users_search/use_get_users_search"; import { SearchInput } from "../../search/search_input/search_input"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const FriendsSearchScreen = () => { const [username, setUsername] = useState(""); const [debouncedUsername] = useDebounce(username, 1000); const { data, loading } = useGetUsersSearch({ input: debouncedUsername }); return ( - + Wyszukiwarka @@ -21,7 +23,7 @@ export const FriendsSearchScreen = () => { Używając powyższego inputa możesz wyszukać użytkownków i dodać ich do znajomych. - + {loading && } {data && data.usersSearch && ( { }))} /> )} - - + + ); }; diff --git a/apps/native/modules/screens/friends/user_profile/user_profile_info_card/user_profile_info_card.tsx b/apps/native/modules/screens/friends/user_profile/user_profile_info_card/user_profile_info_card.tsx index c229b19..f0444cc 100644 --- a/apps/native/modules/screens/friends/user_profile/user_profile_info_card/user_profile_info_card.tsx +++ b/apps/native/modules/screens/friends/user_profile/user_profile_info_card/user_profile_info_card.tsx @@ -1,9 +1,10 @@ import { RefreshCcw } from "@tamagui/lucide-icons"; -import { Card, View, XStack } from "tamagui"; +import { Card, View } from "tamagui"; import { ButtonWithIcon } from "ui/forms/button_icon"; import { Text } from "ui/typography/text"; -import { UserAvatar } from "../../../../user/user_avatar/user_avatar"; +import { UserAvatar } from "@/modules/user/user_avatar/user_avatar"; +import { HStack } from "@/ui/layout/hstack/hstack"; type UserProfileInfoCardProps = { name?: string | null; @@ -18,20 +19,20 @@ export const UserProfileInfoCard = ({ }: UserProfileInfoCardProps) => { return ( - + - + Nazwa: {name} - + } /> - + ); }; diff --git a/apps/native/modules/screens/friends/user_profile/user_profile_screen.tsx b/apps/native/modules/screens/friends/user_profile/user_profile_screen.tsx index fd7b988..3b4bc3a 100644 --- a/apps/native/modules/screens/friends/user_profile/user_profile_screen.tsx +++ b/apps/native/modules/screens/friends/user_profile/user_profile_screen.tsx @@ -1,6 +1,6 @@ import { Filter } from "@tamagui/lucide-icons"; import { Link, useLocalSearchParams } from "expo-router"; -import { ScrollView, Spinner, View, XStack, YStack } from "tamagui"; +import { Spinner, View } from "tamagui"; import { useUserFriendGamesStatus } from "./use_user_friend_games_status/use_user_friend_games_status"; import { useUserProfile } from "./use_user_profile/use_user_profile"; @@ -9,6 +9,9 @@ import { GamesStatusList } from "../../games/games_status_list/games_status_list import { mapGamesStatusToItem } from "../../games/games_status_list/map_games_status_to_item"; import { GamesStatusListSearch } from "../../games/games_status_list_search/games_status_list_search"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const UserProfileScreen = () => { const localSearchParams = useLocalSearchParams<{ oauth_id: string }>(); const { oauth_id } = localSearchParams; @@ -27,7 +30,7 @@ export const UserProfileScreen = () => { userFriendGamesStatus.data?.userFriendGamesStatus.userGamesStatus || [], ); return ( - + {!userProfileQuery.loading && userProfileQuery.data ? ( { ) : ( )} - + {
- + { items={items} oauthId={userProfileQuery.data?.user.oauthId} /> - + ); }; diff --git a/apps/native/modules/screens/game/game_info/game_info.tsx b/apps/native/modules/screens/game/game_info/game_info.tsx index 7bb3682..7ed9218 100644 --- a/apps/native/modules/screens/game/game_info/game_info.tsx +++ b/apps/native/modules/screens/game/game_info/game_info.tsx @@ -1,6 +1,8 @@ -import { Card, XStack } from "tamagui"; +import { Card } from "tamagui"; import { Text } from "ui/typography/text"; +import { HStack } from "@/ui/layout/hstack/hstack"; + type GameInfoProps = { game: { name: string; @@ -16,7 +18,7 @@ export const GameInfo = ({ game }: GameInfoProps) => { {game.name} - + {game.releaseYear && ( {game.releaseYear} @@ -40,7 +42,7 @@ export const GameInfo = ({ game }: GameInfoProps) => { : platform} ))} - + ); diff --git a/apps/native/modules/screens/game/game_preparing_info/game_preparing_info.tsx b/apps/native/modules/screens/game/game_preparing_info/game_preparing_info.tsx index 5dca38a..4e65cc8 100644 --- a/apps/native/modules/screens/game/game_preparing_info/game_preparing_info.tsx +++ b/apps/native/modules/screens/game/game_preparing_info/game_preparing_info.tsx @@ -1,6 +1,8 @@ -import { Button, YStack } from "tamagui"; +import { Button } from "tamagui"; import { Text } from "ui/typography/text"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type GamePreparingInfoProps = { onRefreshClick: () => void; }; @@ -9,7 +11,7 @@ export const GamePreparingInfo = ({ onRefreshClick, }: GamePreparingInfoProps) => { return ( - + Gra jest aktualnie pobierana do naszej bazy. @@ -24,6 +26,6 @@ export const GamePreparingInfo = ({ > Odśwież - + ); }; diff --git a/apps/native/modules/screens/game/game_screen.tsx b/apps/native/modules/screens/game/game_screen.tsx index 656aa48..12fd37f 100644 --- a/apps/native/modules/screens/game/game_screen.tsx +++ b/apps/native/modules/screens/game/game_screen.tsx @@ -1,6 +1,5 @@ -import { useLocalSearchParams, useNavigation } from "expo-router"; -import { useEffect } from "react"; -import { Spinner, YStack, ScrollView } from "tamagui"; +import { useLocalSearchParams } from "expo-router"; +import { Spinner, ScrollView } from "tamagui"; import { GameCompletionTime } from "./game_completion_time/game_completion_time"; import { GameImage } from "./game_image/game_image"; @@ -10,6 +9,8 @@ import { GameTabs } from "./game_tabs/game_tabs"; import { useGetGameInfo } from "./use_get_game_info/use_get_game_info"; import { useSetHeaderTitle } from "../../router/use_set_header_title"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type GameScreenProps = { redirect: { addToGameStatusUrl: string; @@ -34,14 +35,14 @@ export const GameScreen = ({ redirect }: GameScreenProps) => { const game = gameQuery.data.game; return ( - - + + - + { completionist={game.completionTime?.completionist} mainExtra={game.completionTime?.mainExtra} /> - + ); }; diff --git a/apps/native/modules/screens/game/game_tabs/game_tabs.tsx b/apps/native/modules/screens/game/game_tabs/game_tabs.tsx index e17cce9..e869a52 100644 --- a/apps/native/modules/screens/game/game_tabs/game_tabs.tsx +++ b/apps/native/modules/screens/game/game_tabs/game_tabs.tsx @@ -1,8 +1,10 @@ -import { Check, Plus } from "@tamagui/lucide-icons"; +import { Check } from "@tamagui/lucide-icons"; import { router } from "expo-router"; -import { Card, Separator, View, YStack } from "tamagui"; +import { Card, View } from "tamagui"; import { Text } from "ui/typography/text"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type GameTabsProps = { game: { name: string; @@ -14,10 +16,6 @@ type GameTabsProps = { }; export const GameTabs = ({ game, redirect }: GameTabsProps) => { - const redirectToCollectionAddForm = () => { - router.push(`/collection/collection_add_form/${game.hltbId}`); - }; - const redirectToGamesStatusAddForm = () => { router.push(`${redirect.addToGameStatusUrl}/${game.hltbId}`); }; @@ -36,14 +34,14 @@ export const GameTabs = ({ game, redirect }: GameTabsProps) => { > - + Dodaj do swoich gier - + ); diff --git a/apps/native/modules/screens/game_status_reviews/game_status_reviews_screen.tsx b/apps/native/modules/screens/game_status_reviews/game_status_reviews_screen.tsx index b11a7e8..6f80fba 100644 --- a/apps/native/modules/screens/game_status_reviews/game_status_reviews_screen.tsx +++ b/apps/native/modules/screens/game_status_reviews/game_status_reviews_screen.tsx @@ -1,14 +1,18 @@ import { ChevronRight } from "@tamagui/lucide-icons"; import { router, useLocalSearchParams } from "expo-router"; -import { ScrollView, Separator, Spinner, XStack, YStack } from "tamagui"; +import { ScrollView, Separator, Spinner, XStack } from "tamagui"; import { useGameStatusReviewStore } from "./use_game_status_review_store/use_game_status_review_store"; -import { Text } from "../../../ui/typography/text"; import { truncateString } from "../../strings/truncate_string"; import { UserAvatar } from "../../user/user_avatar/user_avatar"; import { useFriendsGameReviews } from "../user_game_status/user_game_status_friends_reviews/use_friends_game_reviews/use_friends_game_reviews"; import { parseScore } from "../user_game_status/user_game_status_sections/user_game_status_score_section/parse_score"; +import { Pressable } from "@/ui/forms/pressable/pressable"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; +import { Text } from "@/ui/typography/text"; + type GameStatusReviewsScreenProps = { redirect: { review: "friends" | "games"; @@ -55,11 +59,11 @@ export const GameStatusReviewsScreen = ({ }; return ( - + {friendsGameReviews.map((review) => { return ( - { handleReviewClick({ @@ -68,13 +72,13 @@ export const GameStatusReviewsScreen = ({ }); }} > - - + + - + {truncateString(review.profile?.name || "", 20)} @@ -93,15 +97,15 @@ export const GameStatusReviewsScreen = ({ )} - - + + {review.review && } - + - + ); })} - + ); }; diff --git a/apps/native/modules/screens/games/games_screen.tsx b/apps/native/modules/screens/games/games_screen.tsx index 32b3d5d..ae9ce51 100644 --- a/apps/native/modules/screens/games/games_screen.tsx +++ b/apps/native/modules/screens/games/games_screen.tsx @@ -1,6 +1,6 @@ import { Filter } from "@tamagui/lucide-icons"; import { Link } from "expo-router"; -import { View, XStack, YStack } from "tamagui"; +import { View } from "tamagui"; import { GamesStatusCategoriesFab } from "./games_status_categories_fab/games_status_categories_fab"; import { GamesStatusList } from "./games_status_list/games_status_list"; @@ -8,14 +8,17 @@ import { mapGamesStatusToItem } from "./games_status_list/map_games_status_to_it import { GamesStatusListSearch } from "./games_status_list_search/games_status_list_search"; import { useUserGamesStatus } from "./use_user_games_status/use_user_games_status"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const GamesScreen = () => { const gamesStatus = useUserGamesStatus(); const items = mapGamesStatusToItem( gamesStatus.data?.userGamesStatus.userGamesStatus || [], ); return ( - - + + { - + { onRefresh={gamesStatus.onRefresh} /> - + ); }; diff --git a/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx b/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx index e54f7d6..b6ca86f 100644 --- a/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx +++ b/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx @@ -1,12 +1,14 @@ import { useLocalSearchParams } from "expo-router"; import { KeyboardAvoidingView } from "react-native"; -import { Card, Spinner, XStack, Separator, ScrollView } from "tamagui"; +import { Card, Spinner, Separator, ScrollView } from "tamagui"; import { Text } from "ui/typography/text"; import { GamesStatusAddForm } from "./games_status_add_form"; import { truncateString } from "../../../strings/truncate_string"; import { useGetGameInfo } from "../../game/use_get_game_info/use_get_game_info"; +import { HStack } from "@/ui/layout/hstack/hstack"; + export const GamesStatusAddFormScreen = () => { const { hltb_id } = useLocalSearchParams<{ hltb_id: string }>(); const gameQuery = useGetGameInfo(hltb_id); @@ -28,14 +30,14 @@ export const GamesStatusAddFormScreen = () => { height="100%" > - + Aktualnie dodajesz: {truncateString(game.name, 15)} - + diff --git a/apps/native/modules/screens/games/games_status_categories_fab/games_status_categories_fab.tsx b/apps/native/modules/screens/games/games_status_categories_fab/games_status_categories_fab.tsx index 526eff5..f4a81d8 100644 --- a/apps/native/modules/screens/games/games_status_categories_fab/games_status_categories_fab.tsx +++ b/apps/native/modules/screens/games/games_status_categories_fab/games_status_categories_fab.tsx @@ -1,23 +1,17 @@ +import { PlusCircle } from "@tamagui/lucide-icons"; import { router } from "expo-router"; -import { FloatingAction } from "react-native-floating-action"; -import { XStack } from "tamagui"; -import { - GAMES_STATUS_CATEGORIES_FAB_OPTIONS, - ACTION_NAMES, -} from "./games_status_categories_fab_options"; +import { Fab, FabIcon, FabLabel } from "@/ui/overlay/fab/fab"; export const GamesStatusCategoriesFab = () => { return ( - - { - if (name === ACTION_NAMES.ADD_GAME) { - router.push("/games/games_search"); - } - }} - /> - + router.push("/games/games_search")} + > + + Dodaj nową grę + ); }; diff --git a/apps/native/modules/screens/games/games_status_edit_form_screen/games_status_edit_form/games_status_edit_form.tsx b/apps/native/modules/screens/games/games_status_edit_form_screen/games_status_edit_form/games_status_edit_form.tsx index 107dded..81eaa26 100644 --- a/apps/native/modules/screens/games/games_status_edit_form_screen/games_status_edit_form/games_status_edit_form.tsx +++ b/apps/native/modules/screens/games/games_status_edit_form_screen/games_status_edit_form/games_status_edit_form.tsx @@ -1,11 +1,13 @@ import { KeyboardAvoidingView } from "react-native"; -import { Card, ScrollView, Separator, XStack } from "tamagui"; +import { Card, ScrollView, Separator } from "tamagui"; import { Text } from "ui/typography/text"; -import { GamesStatusForm } from "../../../../games_status/games_status_form/games_status_form"; -import { truncateString } from "../../../../strings/truncate_string"; import { UserGameStatusQuery } from "../../../user_game_status/use_user_game_status/user_game_status_query.generated"; +import { GamesStatusForm } from "@/modules/games_status/games_status_form/games_status_form"; +import { truncateString } from "@/modules/strings/truncate_string"; +import { HStack } from "@/ui/layout/hstack/hstack"; + type GamesStatusEditFormProps = { gameStatus: UserGameStatusQuery["userGameStatus"]; }; @@ -23,14 +25,14 @@ export const GamesStatusEditForm = ({ height="100%" > - + Aktualnie edytujesz: {truncateString(gameStatus.game.name, 15)} - + { const { data, loading } = useGetGamesStatusFilters(); const { form, handleSubmit } = useGamesStatusFiltersForm(); if (loading || !data) { return ( - - - + + + ); } return ( - +
- + { - + - + -
+ ); }; diff --git a/apps/native/modules/screens/profile/upload_hltb_migration_document/hltb_document_picker/hltb_document_picker.tsx b/apps/native/modules/screens/profile/upload_hltb_migration_document/hltb_document_picker/hltb_document_picker.tsx index a540602..76672c9 100644 --- a/apps/native/modules/screens/profile/upload_hltb_migration_document/hltb_document_picker/hltb_document_picker.tsx +++ b/apps/native/modules/screens/profile/upload_hltb_migration_document/hltb_document_picker/hltb_document_picker.tsx @@ -1,8 +1,10 @@ -import { Button, Spinner, YStack } from "tamagui"; +import { Button, Spinner } from "tamagui"; import { Text } from "ui/typography/text"; import { useHltbDocumentPicker } from "./use_hltb_document_picker"; -import { truncateString } from "../../../../strings/truncate_string"; + +import { truncateString } from "@/modules/strings/truncate_string"; +import { VStack } from "@/ui/layout/vstack/vstack"; type HltbDocumentPickerProps = { buttonVisible: boolean; @@ -17,7 +19,7 @@ export const HltbDocumentPicker = ({ } if (!buttonVisible) return null; return ( - + + ); +}; diff --git a/apps/native/modules/layouts/header/search/search_button.tsx b/apps/native/modules/layouts/header/search/search_button.tsx deleted file mode 100644 index d579f92..0000000 --- a/apps/native/modules/layouts/header/search/search_button.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Search } from "@tamagui/lucide-icons"; -import { router } from "expo-router"; - -import { ButtonWithIcon } from "@/ui/forms/button_icon"; - -export const SearchButton = () => { - return ( - { - router.push("/search/search"); - }} - icon={} - backgrounded={false} - backgroundColor="transparent" - padding={4} - /> - ); -}; diff --git a/apps/native/modules/layouts/header/user_profile_button.tsx b/apps/native/modules/layouts/header/user_profile_button.tsx index 2e29696..3e8d34f 100644 --- a/apps/native/modules/layouts/header/user_profile_button.tsx +++ b/apps/native/modules/layouts/header/user_profile_button.tsx @@ -1,15 +1,14 @@ import { LogIn } from "@tamagui/lucide-icons"; import { router } from "expo-router"; -import { ButtonWithIcon } from "@/ui/forms/button_icon"; +import { Button, ButtonIcon } from "@/ui/forms/button/button"; export const UserProfileButton = () => { return ( - router.push("auth")} - icon={} - backgroundColor="transparent" - padding={4} - /> + ); }; diff --git a/apps/native/modules/screens/homepage/home_screen.tsx b/apps/native/modules/screens/homepage/home_screen.tsx index 1ba5aac..3507304 100644 --- a/apps/native/modules/screens/homepage/home_screen.tsx +++ b/apps/native/modules/screens/homepage/home_screen.tsx @@ -1,23 +1,32 @@ import { ScrollView } from "tamagui"; -import { GText, Text } from "ui/typography/text"; +import { GText } from "ui/typography/text"; import { FriendsActivity } from "./friends_activity/friends_activity"; import { IncomingGamesCarousel } from "./incoming_games_carousel/incoming_games_carousel"; +import { LastUpdatedGameStatus } from "@/modules/screens/homepage/last_updated/last_update"; +import { Box } from "@/ui/layout/box/box"; +import { Divider } from "@/ui/layout/divider/divider"; import { VStack } from "@/ui/layout/vstack/vstack"; export const HomeScreen = () => { return ( - - + <> + + - - Nadchodzące premiery - - + + + + + + Nadchodzące premiery + + + + - - - + + ); }; diff --git a/apps/native/modules/screens/homepage/homepage_section/homepage_section.tsx b/apps/native/modules/screens/homepage/homepage_section/homepage_section.tsx new file mode 100644 index 0000000..d1e4540 --- /dev/null +++ b/apps/native/modules/screens/homepage/homepage_section/homepage_section.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from "react"; + +import { Box } from "@/ui/layout/box/box"; +import { VStack } from "@/ui/layout/vstack/vstack"; +import { GText } from "@/ui/typography/text"; + +type HomepageSectionProps = { + heading: string; + children: ReactNode; +}; + +export const HomepageSection = ({ + heading, + children, +}: HomepageSectionProps) => { + return ( + + + {heading} + + {children} + + ); +}; diff --git a/apps/native/modules/screens/homepage/homepage_section/homepage_section_carousel.tsx b/apps/native/modules/screens/homepage/homepage_section/homepage_section_carousel.tsx new file mode 100644 index 0000000..1095ddc --- /dev/null +++ b/apps/native/modules/screens/homepage/homepage_section/homepage_section_carousel.tsx @@ -0,0 +1,30 @@ +import { ReactElement } from "react"; + +import { Carousel, CarouselRenderItemInfo } from "@/ui/data-display/carousel"; +import { Box } from "@/ui/layout/box/box"; + +type HomepageSectionCarouselProps = { + renderItem: (info: CarouselRenderItemInfo) => ReactElement; + data: T[]; + itemWidth?: number; + itemSpacing?: number; +}; + +export const HomepageSectionCarousel = ({ + renderItem, + data, + itemWidth = 150, + itemSpacing, +}: HomepageSectionCarouselProps) => { + return ( + + + + ); +}; diff --git a/apps/native/modules/screens/homepage/incoming_games_carousel/incoming_games_carousel.tsx b/apps/native/modules/screens/homepage/incoming_games_carousel/incoming_games_carousel.tsx index 99f7a25..6ff1583 100644 --- a/apps/native/modules/screens/homepage/incoming_games_carousel/incoming_games_carousel.tsx +++ b/apps/native/modules/screens/homepage/incoming_games_carousel/incoming_games_carousel.tsx @@ -7,6 +7,7 @@ import { VStack } from "@/ui/layout/vstack/vstack"; export const IncomingGamesCarousel = () => { return ( game.game?.cover?.url)} renderItem={({ item }) => { return ( diff --git a/apps/native/modules/screens/homepage/last_updated/last_update.tsx b/apps/native/modules/screens/homepage/last_updated/last_update.tsx new file mode 100644 index 0000000..06497fd --- /dev/null +++ b/apps/native/modules/screens/homepage/last_updated/last_update.tsx @@ -0,0 +1,95 @@ +import { HomepageSection } from "@/modules/screens/homepage/homepage_section/homepage_section"; +import { HomepageSectionCarousel } from "@/modules/screens/homepage/homepage_section/homepage_section_carousel"; +import { truncateString } from "@/modules/strings/truncate_string"; +import { Box } from "@/ui/layout/box/box"; +import { VStack } from "@/ui/layout/vstack/vstack"; +import { Image } from "@/ui/media_and_icons/image/image"; +import { GText } from "@/ui/typography/text"; +const sampleData = [ + { + gameImage: "https://howlongtobeat.com/games/62941_Hades.jpg?width=760", + gameTitle: "Hades", + gameStatus: "W trakcie", + }, + { + gameImage: "https://howlongtobeat.com/games/62941_Hades.jpg?width=760", + gameTitle: "Cyberpunk 2077Cyberpunk 2077", + gameStatus: "Ukończona", + }, + { + gameImage: "https://howlongtobeat.com/games/62941_Hades.jpg?width=760", + gameTitle: "Gra 3", + gameStatus: "Porzucona", + }, + { + gameImage: "https://howlongtobeat.com/games/62941_Hades.jpg?width=760", + gameTitle: "Gra 4", + gameStatus: "Backlog", + }, +]; + +export const LastUpdatedGameStatus = () => { + return ( + + + + ); +}; + +type CurrentlyPlayingCarouselItemProps = { + item: { + gameImage: string; + gameTitle: string; + gameStatus: string; + }; +}; + +const CurrentlyPlayingCarouselItem = ({ + item, +}: CurrentlyPlayingCarouselItemProps) => { + return ( + + + {item.gameTitle} + + + {truncateString(item.gameTitle, 15)} + + + {item.gameStatus} + + + + + ); +}; diff --git a/apps/native/package.json b/apps/native/package.json index 2c99f2c..98f558b 100644 --- a/apps/native/package.json +++ b/apps/native/package.json @@ -58,6 +58,7 @@ "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "graphql": "^16.8.1", + "lucide-react-native": "^0.563.0", "moti": "^0.30.0", "nativewind": "^4.1.23", "react": "19.1.0", diff --git a/apps/native/tailwind.config.js b/apps/native/tailwind.config.js index ad2c837..4da99dc 100644 --- a/apps/native/tailwind.config.js +++ b/apps/native/tailwind.config.js @@ -19,6 +19,44 @@ module.exports = { }, ], theme: { + colors: { + dark: { + 900: "#0B0B0B", // opcjonalnie bardzo ciemne tło + 800: "#121212", // główne tło aplikacji + 700: "#1B1B23FF", // ciemniejszy panel/karta + }, + surface: { + 900: "#141414", // alternatywne tła kart + 800: "#1A1A1A", + }, + accent: { + purple: "#6200EA", // neonowy fiolet (główny accent) + blue: "#00B0FF", // elektryzujący błękit (alternatywa) + }, + text: { + primary: "#FFFFFF", // główny tekst + muted: "#B3B3B3", // jasnoszary tekst + subtle: "#9E9E9E", // jeszcze bardziej stonowany + }, + card: { + bg: "#151515", // tło kart + border: "#262626", // delikatna ramka/podział + }, + overlay: { + soft: "rgba(0,0,0,0.45)", // przyciemnienie obrazów/hero + glass: "rgba(255,255,255,0.03)", // subtelne szkło + }, + neon: { + "purple-glow": "#8A5CFF66", // półprzezroczyste do glow + "blue-glow": "#00C8FF66", + }, + danger: { + 500: "#FF4D4F", + }, + success: { + 500: "#4CAF50", + }, + }, extend: { colors: { primary: { diff --git a/apps/native/ui/data-display/carousel.tsx b/apps/native/ui/data-display/carousel.tsx index 852595a..66427b5 100644 --- a/apps/native/ui/data-display/carousel.tsx +++ b/apps/native/ui/data-display/carousel.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef, useState } from "react"; import { - Dimensions, NativeScrollEvent, NativeSyntheticEvent, ScrollView, @@ -8,7 +7,7 @@ import { import { Box } from "../layout/box/box"; -type CarouselRenderItemInfo = { +export type CarouselRenderItemInfo = { item: T; index: number; }; @@ -16,21 +15,23 @@ type CarouselRenderItemInfo = { type CarouselProps = { data: T[]; renderItem: (info: CarouselRenderItemInfo) => React.ReactElement; + itemWidth: number; autoPlay?: boolean; autoPlayInterval?: number; loop?: boolean; + itemSpacing?: number; }; -const { width } = Dimensions.get("window"); -const ITEM_WIDTH = width / 3; const ITEM_SPACING = 16; export const Carousel = ({ data, renderItem, + itemWidth, autoPlay = true, autoPlayInterval = 3000, loop = true, + itemSpacing = ITEM_SPACING, }: CarouselProps) => { const scrollViewRef = useRef(null); const [currentIndex, setCurrentIndex] = useState(0); @@ -51,7 +52,7 @@ export const Carousel = ({ } } else { scrollViewRef.current?.scrollTo({ - x: (ITEM_WIDTH + ITEM_SPACING) * nextIndex, + x: (itemWidth + itemSpacing) * nextIndex, animated: true, }); setCurrentIndex(nextIndex); @@ -76,7 +77,7 @@ export const Carousel = ({ // Handle scroll position to update current index const handleScroll = (event: NativeSyntheticEvent) => { const scrollPosition = event.nativeEvent.contentOffset.x; - const index = Math.round(scrollPosition / (ITEM_WIDTH + ITEM_SPACING)); + const index = Math.round(scrollPosition / (itemWidth + itemSpacing)); if (index !== currentIndex && index >= 0 && index < data.length) { setCurrentIndex(index); } @@ -107,19 +108,25 @@ export const Carousel = ({ ref={scrollViewRef} horizontal showsHorizontalScrollIndicator={false} - snapToInterval={ITEM_WIDTH + ITEM_SPACING} + snapToInterval={itemWidth + itemSpacing} decelerationRate="fast" onScroll={handleScroll} scrollEventThrottle={16} onScrollBeginDrag={onScrollBeginDrag} onScrollEndDrag={onScrollEndDrag} contentContainerStyle={{ + paddingLeft: 16, paddingRight: 16, - gap: 16, }} > {data.map((item, index) => ( - + {renderItem({ item, index })} ))} diff --git a/apps/native/ui/forms/button/button.tsx b/apps/native/ui/forms/button/button.tsx new file mode 100644 index 0000000..c21dc00 --- /dev/null +++ b/apps/native/ui/forms/button/button.tsx @@ -0,0 +1,433 @@ +import { createButton } from "@gluestack-ui/core/button/creator"; +import { PrimitiveIcon, UIIcon } from "@gluestack-ui/core/icon/creator"; +import { + tva, + withStyleContext, + useStyleContext, + type VariantProps, +} from "@gluestack-ui/utils/nativewind-utils"; +import { cssInterop } from "nativewind"; +import React from "react"; +import { ActivityIndicator, Pressable, Text, View } from "react-native"; + +const SCOPE = "BUTTON"; + +const Root = withStyleContext(Pressable, SCOPE); + +const UIButton = createButton({ + Root, + Text, + Group: View, + Spinner: ActivityIndicator, + Icon: UIIcon, +}); + +cssInterop(PrimitiveIcon, { + className: { + target: "style", + nativeStyleToProp: { + height: true, + width: true, + fill: true, + color: "classNameColor", + stroke: true, + }, + }, +}); + +const buttonStyle = tva({ + base: "group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2", + variants: { + action: { + primary: + "bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info", + secondary: + "bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info", + positive: + "bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info", + negative: + "bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info", + default: + "bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent", + }, + variant: { + link: "px-0", + outline: + "bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent", + solid: "", + }, + + size: { + xs: "px-3.5 h-8", + sm: "px-4 h-9", + md: "px-5 h-10", + lg: "px-6 h-11", + xl: "px-7 h-12", + }, + }, + compoundVariants: [ + { + action: "primary", + variant: "link", + class: + "px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent", + }, + { + action: "secondary", + variant: "link", + class: + "px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent", + }, + { + action: "positive", + variant: "link", + class: + "px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent", + }, + { + action: "negative", + variant: "link", + class: + "px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent", + }, + { + action: "primary", + variant: "outline", + class: + "bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent", + }, + { + action: "secondary", + variant: "outline", + class: + "bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent", + }, + { + action: "positive", + variant: "outline", + class: + "bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent", + }, + { + action: "negative", + variant: "outline", + class: + "bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent", + }, + ], +}); + +const buttonTextStyle = tva({ + base: "text-typography-0 font-semibold web:select-none", + parentVariants: { + action: { + primary: + "text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700", + secondary: + "text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700", + positive: + "text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700", + negative: + "text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700", + }, + variant: { + link: "data-[hover=true]:underline data-[active=true]:underline", + outline: "", + solid: + "text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0", + }, + size: { + xs: "text-xs", + sm: "text-sm", + md: "text-base", + lg: "text-lg", + xl: "text-xl", + }, + }, + parentCompoundVariants: [ + { + variant: "solid", + action: "primary", + class: + "text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0", + }, + { + variant: "solid", + action: "secondary", + class: + "text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800", + }, + { + variant: "solid", + action: "positive", + class: + "text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0", + }, + { + variant: "solid", + action: "negative", + class: + "text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0", + }, + { + variant: "outline", + action: "primary", + class: + "text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500", + }, + { + variant: "outline", + action: "secondary", + class: + "text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700", + }, + { + variant: "outline", + action: "positive", + class: + "text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500", + }, + { + variant: "outline", + action: "negative", + class: + "text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500", + }, + ], +}); + +const buttonIconStyle = tva({ + base: "fill-none", + parentVariants: { + variant: { + link: "data-[hover=true]:underline data-[active=true]:underline", + outline: "", + solid: + "text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0", + }, + size: { + xs: "h-3.5 w-3.5", + sm: "h-4 w-4", + md: "h-[18px] w-[18px]", + lg: "h-[18px] w-[18px]", + xl: "h-5 w-5", + }, + action: { + primary: + "text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700", + secondary: + "text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700", + positive: + "text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700", + + negative: + "text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700", + }, + }, + parentCompoundVariants: [ + { + variant: "solid", + action: "primary", + class: + "text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0", + }, + { + variant: "solid", + action: "secondary", + class: + "text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800", + }, + { + variant: "solid", + action: "positive", + class: + "text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0", + }, + { + variant: "solid", + action: "negative", + class: + "text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0", + }, + ], +}); + +const buttonGroupStyle = tva({ + base: "", + variants: { + space: { + xs: "gap-1", + sm: "gap-2", + md: "gap-3", + lg: "gap-4", + xl: "gap-5", + "2xl": "gap-6", + "3xl": "gap-7", + "4xl": "gap-8", + }, + isAttached: { + true: "gap-0", + }, + flexDirection: { + row: "flex-row", + column: "flex-col", + "row-reverse": "flex-row-reverse", + "column-reverse": "flex-col-reverse", + }, + }, +}); + +type IButtonProps = Omit< + React.ComponentPropsWithoutRef, + "context" +> & + VariantProps & { className?: string }; + +const Button = React.forwardRef< + React.ElementRef, + IButtonProps +>( + ( + { className, variant = "solid", size = "md", action = "primary", ...props }, + ref, + ) => { + return ( + + ); + }, +); + +type IButtonTextProps = React.ComponentPropsWithoutRef & + VariantProps & { className?: string }; + +const ButtonText = React.forwardRef< + React.ElementRef, + IButtonTextProps +>(({ className, variant, size, action, ...props }, ref) => { + const { + variant: parentVariant, + size: parentSize, + action: parentAction, + } = useStyleContext(SCOPE); + + return ( + + ); +}); + +const ButtonSpinner = UIButton.Spinner; + +type IButtonIcon = React.ComponentPropsWithoutRef & + VariantProps & { + className?: string | undefined; + as?: React.ElementType; + height?: number; + width?: number; + }; + +const ButtonIcon = React.forwardRef< + React.ElementRef, + IButtonIcon +>(({ className, size, ...props }, ref) => { + const { + variant: parentVariant, + size: parentSize, + action: parentAction, + } = useStyleContext(SCOPE); + + if (typeof size === "number") { + return ( + + ); + } else if ( + (props.height !== undefined || props.width !== undefined) && + size === undefined + ) { + return ( + + ); + } + return ( + + ); +}); + +type IButtonGroupProps = React.ComponentPropsWithoutRef & + VariantProps; + +const ButtonGroup = React.forwardRef< + React.ElementRef, + IButtonGroupProps +>( + ( + { + className, + space = "md", + isAttached = false, + flexDirection = "column", + ...props + }, + ref, + ) => { + return ( + + ); + }, +); + +Button.displayName = "Button"; +ButtonText.displayName = "ButtonText"; +ButtonSpinner.displayName = "ButtonSpinner"; +ButtonIcon.displayName = "ButtonIcon"; +ButtonGroup.displayName = "ButtonGroup"; + +export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup }; diff --git a/apps/native/ui/layout/divider/divider.tsx b/apps/native/ui/layout/divider/divider.tsx new file mode 100644 index 0000000..70f308c --- /dev/null +++ b/apps/native/ui/layout/divider/divider.tsx @@ -0,0 +1,39 @@ +import { tva } from "@gluestack-ui/utils/nativewind-utils"; +import type { VariantProps } from "@gluestack-ui/utils/nativewind-utils"; +import React from "react"; +import { Platform, View } from "react-native"; + +const dividerStyle = tva({ + base: "bg-background-200", + variants: { + orientation: { + vertical: "w-px h-full", + horizontal: "h-px w-full", + }, + }, +}); + +type IUIDividerProps = React.ComponentPropsWithoutRef & + VariantProps; + +const Divider = React.forwardRef< + React.ComponentRef, + IUIDividerProps +>(function Divider({ className, orientation = "horizontal", ...props }, ref) { + return ( + + ); +}); + +Divider.displayName = "Divider"; + +export { Divider }; diff --git a/apps/native/ui/media_and_icons/avatar/avatar.tsx b/apps/native/ui/media_and_icons/avatar/avatar.tsx new file mode 100644 index 0000000..269f990 --- /dev/null +++ b/apps/native/ui/media_and_icons/avatar/avatar.tsx @@ -0,0 +1,182 @@ +import { createAvatar } from "@gluestack-ui/core/avatar/creator"; +import { + tva, + withStyleContext, + useStyleContext, +} from "@gluestack-ui/utils/nativewind-utils"; +import type { VariantProps } from "@gluestack-ui/utils/nativewind-utils"; +import React from "react"; +import { View, Text, Image, Platform } from "react-native"; +const SCOPE = "AVATAR"; + +const UIAvatar = createAvatar({ + Root: withStyleContext(View, SCOPE), + Badge: View, + Group: View, + Image, + FallbackText: Text, +}); + +const avatarStyle = tva({ + base: "rounded-full justify-center items-center relative bg-primary-600 group-[.avatar-group]/avatar-group:-ml-2.5", + variants: { + size: { + xs: "w-6 h-6", + sm: "w-8 h-8", + md: "w-12 h-12", + lg: "w-16 h-16", + xl: "w-24 h-24", + "2xl": "w-32 h-32", + }, + }, +}); + +const avatarFallbackTextStyle = tva({ + base: "text-typography-0 font-semibold overflow-hidden text-transform:uppercase web:cursor-default", + + parentVariants: { + size: { + xs: "text-2xs", + sm: "text-xs", + md: "text-base", + lg: "text-xl", + xl: "text-3xl", + "2xl": "text-5xl", + }, + }, +}); + +const avatarGroupStyle = tva({ + base: "group/avatar-group flex-row-reverse relative avatar-group", +}); + +const avatarBadgeStyle = tva({ + base: "w-5 h-5 bg-success-500 rounded-full absolute right-0 bottom-0 border-background-0 border-2", + parentVariants: { + size: { + xs: "w-2 h-2", + sm: "w-2 h-2", + md: "w-3 h-3", + lg: "w-4 h-4", + xl: "w-6 h-6", + "2xl": "w-8 h-8", + }, + }, +}); + +const avatarImageStyle = tva({ + base: "h-full w-full rounded-full absolute", +}); + +type IAvatarProps = Omit< + React.ComponentPropsWithoutRef, + "context" +> & + VariantProps; + +const Avatar = React.forwardRef< + React.ComponentRef, + IAvatarProps +>(function Avatar({ className, size = "md", ...props }, ref) { + return ( + + ); +}); + +type IAvatarBadgeProps = React.ComponentPropsWithoutRef & + VariantProps; + +const AvatarBadge = React.forwardRef< + React.ComponentRef, + IAvatarBadgeProps +>(function AvatarBadge({ className, size, ...props }, ref) { + const { size: parentSize } = useStyleContext(SCOPE); + + return ( + + ); +}); + +type IAvatarFallbackTextProps = React.ComponentPropsWithoutRef< + typeof UIAvatar.FallbackText +> & + VariantProps; +const AvatarFallbackText = React.forwardRef< + React.ComponentRef, + IAvatarFallbackTextProps +>(function AvatarFallbackText({ className, size, ...props }, ref) { + const { size: parentSize } = useStyleContext(SCOPE); + + return ( + + ); +}); + +type IAvatarImageProps = React.ComponentPropsWithoutRef & + VariantProps; + +const AvatarImage = React.forwardRef< + React.ComponentRef, + IAvatarImageProps +>(function AvatarImage({ className, ...props }, ref) { + return ( + + ); +}); + +type IAvatarGroupProps = React.ComponentPropsWithoutRef & + VariantProps; + +const AvatarGroup = React.forwardRef< + React.ComponentRef, + IAvatarGroupProps +>(function AvatarGroup({ className, ...props }, ref) { + return ( + + ); +}); + +export { Avatar, AvatarBadge, AvatarFallbackText, AvatarImage, AvatarGroup }; diff --git a/apps/native/ui/media_and_icons/image/image.tsx b/apps/native/ui/media_and_icons/image/image.tsx new file mode 100644 index 0000000..0ac5740 --- /dev/null +++ b/apps/native/ui/media_and_icons/image/image.tsx @@ -0,0 +1,48 @@ +import { createImage } from "@gluestack-ui/core/image/creator"; +import { tva } from "@gluestack-ui/utils/nativewind-utils"; +import type { VariantProps } from "@gluestack-ui/utils/nativewind-utils"; +import React from "react"; +import { Platform, Image as RNImage } from "react-native"; + +const imageStyle = tva({ + base: "max-w-full", + variants: { + size: { + "2xs": "h-6 w-6", + xs: "h-10 w-10", + sm: "h-16 w-16", + md: "h-20 w-20", + lg: "h-24 w-24", + xl: "h-32 w-32", + "2xl": "h-64 w-64", + full: "h-full w-full", + none: "", + }, + }, +}); + +const UIImage = createImage({ Root: RNImage }); + +type ImageProps = VariantProps & + React.ComponentProps; +const Image = React.forwardRef< + React.ComponentRef, + ImageProps & { className?: string } +>(function Image({ size = "md", className, ...props }, ref) { + return ( + + ); +}); + +Image.displayName = "Image"; +export { Image }; diff --git a/yarn.lock b/yarn.lock index cacebef..24bdb1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19060,6 +19060,7 @@ __metadata: expo-splash-screen: "npm:~31.0.13" expo-status-bar: "npm:~3.0.9" graphql: "npm:^16.8.1" + lucide-react-native: "npm:^0.563.0" moti: "npm:^0.30.0" nativewind: "npm:^4.1.23" prettier-plugin-tailwindcss: "npm:^0.5.11" @@ -22371,6 +22372,17 @@ __metadata: languageName: node linkType: hard +"lucide-react-native@npm:^0.563.0": + version: 0.563.0 + resolution: "lucide-react-native@npm:0.563.0" + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-native: "*" + react-native-svg: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 + checksum: 10c0/f74dbd343d322ee625b4a48d4b0e4672d9be1561d7818a17007011139a1a1215ff956e6736a58f6c85f0e6a0a5e48d8c2f239b9b373697a1b4169937468e9a36 + languageName: node + linkType: hard + "lucide-react@npm:0.468.0": version: 0.468.0 resolution: "lucide-react@npm:0.468.0" From 65c3ebd4ee5119bacc3821d3b224868cd52740e6 Mon Sep 17 00:00:00 2001 From: survikrowa Date: Fri, 30 Jan 2026 23:15:09 +0100 Subject: [PATCH 4/8] feat(native/api): add last edited games feature with GraphQL integration and UI updates --- .../modules/games_status/games_status.dto.ts | 13 ++++ .../games_status/games_status.module.ts | 2 + .../games_status/games_status.resolver.ts | 21 +++++- .../get_last_edited_games.handler.ts | 47 ++++++++++++ .../get_last_edited_games.query.ts | 24 +++++++ apps/api/src/schema.gql | 9 +++ apps/native/.gitignore | 7 +- apps/native/app.json | 17 ++--- .../native/app/(app)/(authorized)/_layout.tsx | 24 +++++++ .../games_status_info/[games_status_id].tsx | 37 ++++++++++ .../(tabs)/(authorized)/games/_layout.tsx | 7 +- .../games_status_info/[games_status_id].tsx | 16 ----- apps/native/app/(app)/(tabs)/home.tsx | 4 +- apps/native/app/_layout.tsx | 4 ++ apps/native/assets/splash-2.png | Bin 0 -> 121664 bytes apps/native/assets/splash.png | Bin 47346 -> 0 bytes .../games_status/map_game_status_to_label.ts | 16 +++++ .../layouts/go_back_header/go_back_header.tsx | 22 +++--- .../games_status_add_form_screen.tsx | 2 +- .../games_status_filters.generated.ts | 2 +- .../modules/screens/homepage/home_screen.tsx | 20 +++++- .../homepage/last_updated/last_update.tsx | 68 +++++++++++------- .../last_edited_games.generated.ts | 58 +++++++++++++++ .../last_edited_games.graphql | 10 +++ .../use_last_edited_games.ts | 18 +++++ .../user_game_status_screen.tsx | 9 +-- ...er_game_status_game_completion_section.tsx | 6 +- apps/native/tsconfig.json | 23 ++++-- apps/native/ui/data-display/carousel.tsx | 4 -- 29 files changed, 397 insertions(+), 93 deletions(-) create mode 100644 apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.handler.ts create mode 100644 apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.query.ts create mode 100644 apps/native/app/(app)/(authorized)/_layout.tsx create mode 100644 apps/native/app/(app)/(authorized)/games_status/games_status_info/[games_status_id].tsx delete mode 100644 apps/native/app/(app)/(tabs)/(authorized)/games/games_status_info/[games_status_id].tsx create mode 100644 apps/native/assets/splash-2.png delete mode 100644 apps/native/assets/splash.png create mode 100644 apps/native/modules/games_status/map_game_status_to_label.ts create mode 100644 apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated.ts create mode 100644 apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.graphql create mode 100644 apps/native/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games.ts diff --git a/apps/api/src/modules/games_status/games_status.dto.ts b/apps/api/src/modules/games_status/games_status.dto.ts index cada572..349b12b 100644 --- a/apps/api/src/modules/games_status/games_status.dto.ts +++ b/apps/api/src/modules/games_status/games_status.dto.ts @@ -9,6 +9,7 @@ import { PlatformDTO } from '../platforms/platforms.dto'; import { GameWithAllDataDTO } from '../games/games.dto'; import { PaginationDTO } from '../pagination/pagination.dto'; import { ProfileInfoDTO } from '../profiles/profiles.dto'; +import { CoverDTO } from '../covers/covers.dto'; registerEnumType(GameStatus, { name: 'gameStatus', @@ -203,3 +204,15 @@ export class FriendsGameStatusReviewsDTO { @Field(() => String, { nullable: true }) score: string | null; } + +@ObjectType({ description: 'User last edited games statuses' }) +export class LastEditedGamesStatusDTO { + @Field(() => Number) + id: number; + @Field(() => String) + name: string; + @Field(() => CoverDTO, { nullable: true }) + cover: CoverDTO | null; + @Field(() => GameStatus) + status: GameStatus; +} diff --git a/apps/api/src/modules/games_status/games_status.module.ts b/apps/api/src/modules/games_status/games_status.module.ts index f8144d4..bfe2d06 100644 --- a/apps/api/src/modules/games_status/games_status.module.ts +++ b/apps/api/src/modules/games_status/games_status.module.ts @@ -8,9 +8,11 @@ import { AuthModule } from '../auth/auth.module'; import { CommandHandlerType, CqrsModule, QueryHandlerType } from '@nestjs/cqrs'; import { GetAllUserGamesStatusByOauthIdHandler } from './queries/get_all_user_games_status_by_oauthid/get_all_user_games_status_by_oauthid.handler'; import { RemoveUserGameStatusByUserOauthIdHandler } from './commands/remove_user_game_status_by_user_oauth_id/remove_user_game_status_by_user_oauth_id.handler'; +import { GetLastEditedGamesHandler } from './queries/get_last_edited_games/get_last_edited_games.handler'; const QueryHandlers: QueryHandlerType[] = [ GetAllUserGamesStatusByOauthIdHandler, + GetLastEditedGamesHandler, ]; const CommandHandlers: CommandHandlerType[] = [ diff --git a/apps/api/src/modules/games_status/games_status.resolver.ts b/apps/api/src/modules/games_status/games_status.resolver.ts index 1cf9d56..bcc0b95 100644 --- a/apps/api/src/modules/games_status/games_status.resolver.ts +++ b/apps/api/src/modules/games_status/games_status.resolver.ts @@ -1,4 +1,4 @@ -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UpsertGameStatusArgsDTO, GameStatusSuccessResponseDTO, @@ -9,6 +9,7 @@ import { SortOptionsDTO, GameStatusProgressStateDTO, UserFriendGamesStatusResponseWithPaginationDTO, + LastEditedGamesStatusDTO, } from './games_status.dto'; import { GamesStatusService } from './games_status.service'; import { HttpException, HttpStatus, UseGuards } from '@nestjs/common'; @@ -20,10 +21,15 @@ import { GetAllUserGamesStatusArgs, } from './games_status.args'; import { AdminUserGuard } from '../auth/guards/admin-user.guard'; +import { QueryBus } from '@nestjs/cqrs'; +import { GetLastEditedGamesQuery } from './queries/get_last_edited_games/get_last_edited_games.query'; @Resolver() export class GamesStatusResolver { - constructor(private readonly gamesStatusService: GamesStatusService) {} + constructor( + private readonly gamesStatusService: GamesStatusService, + private readonly queryBus: QueryBus, + ) {} @UseGuards(JwtAuthGuard) @Mutation(() => GameStatusSuccessResponseDTO) async upsertGameStatus( @@ -217,4 +223,15 @@ export class GamesStatusResolver { this.gamesStatusService.getAvailableGamesStatusProgressStates(), }; } + + @UseGuards(JwtAuthGuard) + @Query(() => [LastEditedGamesStatusDTO], { + name: 'lastEditedGames', + }) + async getLastEditedGames( + @User() user: UserAuthDTO, + @Args('limit', { type: () => Int, defaultValue: 5 }) limit: number, + ) { + return this.queryBus.execute(new GetLastEditedGamesQuery(user.sub, limit)); + } } diff --git a/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.handler.ts b/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.handler.ts new file mode 100644 index 0000000..a1abf0c --- /dev/null +++ b/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.handler.ts @@ -0,0 +1,47 @@ +import { PrismaService } from '../../../database/prisma.service'; +import { + GetLastEditedGamesQuery, + GetLastEditedGamesQueryResponse, +} from './get_last_edited_games.query'; +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; + +@QueryHandler(GetLastEditedGamesQuery) +export class GetLastEditedGamesHandler + implements + IQueryHandler +{ + constructor(private readonly prisma: PrismaService) {} + + async execute( + query: GetLastEditedGamesQuery, + ): Promise { + const { userId, limit } = query; + const games = await this.prisma.gamesStatus.findMany({ + where: { + user: { + oauthId: userId, + }, + }, + orderBy: { updatedAt: 'desc' }, + take: limit, + select: { + status: true, + id: true, + game: { + select: { + id: true, + name: true, + cover: true, + }, + }, + }, + }); + console.log(games); + return games.map((gs) => ({ + id: gs.id, + name: gs.game.name, + cover: gs.game.cover, + status: gs.status, + })); + } +} diff --git a/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.query.ts b/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.query.ts new file mode 100644 index 0000000..f29ca0a --- /dev/null +++ b/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.query.ts @@ -0,0 +1,24 @@ +import { Query } from '@nestjs/cqrs'; + +export class GetLastEditedGamesQuery extends Query< + GetLastEditedGamesQueryResponse[] +> { + constructor( + public readonly userId: string, + public readonly limit: number = 5, + ) { + super(); + } +} + +export type GetLastEditedGamesQueryResponse = { + id: number; + name: string; + status: string; + cover: { + id: number; + bigUrl: string; + mediumUrl: string; + smallUrl: string; + } | null; +}; diff --git a/apps/api/src/schema.gql b/apps/api/src/schema.gql index dd31311..733d6c2 100644 --- a/apps/api/src/schema.gql +++ b/apps/api/src/schema.gql @@ -189,6 +189,14 @@ type HowLongToBeatMigrationStatusDTO { status: MigrationStatus } +"""User last edited games statuses""" +type LastEditedGamesStatusDTO { + cover: CoverDTO + id: Float! + name: String! + status: GameStatus! +} + """The status of the migration of a user's HowLongToBeat account""" enum MigrationStatus { FAILED @@ -297,6 +305,7 @@ type Query { gamesStatusSortOptions: SortOptionsDTO! getAllUserGamesStatusByOauthId(oauthId: String!): [UserGamesStatusResponseDTO!]! getProfileCollections: [CollectionDTO!]! + lastEditedGames(limit: Int! = 5): [LastEditedGamesStatusDTO!]! migrationStatus: HowLongToBeatMigrationStatusDTO! """Get user and friends game status reviews""" diff --git a/apps/native/.gitignore b/apps/native/.gitignore index 12bdb34..f12a21f 100644 --- a/apps/native/.gitignore +++ b/apps/native/.gitignore @@ -27,4 +27,9 @@ __generated__/* .env.development .env.test .env.production -eas.json \ No newline at end of file +eas.json +# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb +# The following patterns were generated by expo-cli + +expo-env.d.ts +# @end expo-cli \ No newline at end of file diff --git a/apps/native/app.json b/apps/native/app.json index 0bb0692..656d736 100644 --- a/apps/native/app.json +++ b/apps/native/app.json @@ -8,20 +8,19 @@ "icon": "./assets/images/icon.png", "owner": "game-critique", "userInterfaceStyle": "light", + "newArchEnabled": true, "experiments": { - "tsconfigPaths": true + "tsconfigPaths": true, + "typedRoutes": true }, "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" + "image": "./assets/splash-2.png", + "resizeMode": "contain" }, "updates": { "fallbackToCacheTimeout": 0 }, - "assetBundlePatterns": [ - "**/*" - ], + "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true, "bundleIdentifier": "com.gamecritique.mobile" @@ -32,9 +31,7 @@ "backgroundColor": "#FFFFFF" }, "package": "com.gamecritique.mobile", - "permissions": [ - "INTERNET" - ] + "permissions": ["INTERNET"] }, "plugins": [ [ diff --git a/apps/native/app/(app)/(authorized)/_layout.tsx b/apps/native/app/(app)/(authorized)/_layout.tsx new file mode 100644 index 0000000..0f43939 --- /dev/null +++ b/apps/native/app/(app)/(authorized)/_layout.tsx @@ -0,0 +1,24 @@ +import { Redirect, Stack } from "expo-router"; +import { useAuth0 } from "react-native-auth0"; + +import { SkeletonText } from "@/ui/feedback/skeleton/skeleton"; +import { Box } from "@/ui/layout/box/box"; + +const AppAuthorizedLayout = () => { + const { user, isLoading } = useAuth0(); + + if (isLoading) { + return ( + + + + ); + } + + if (!user) { + return ; + } + return ; +}; + +export default AppAuthorizedLayout; diff --git a/apps/native/app/(app)/(authorized)/games_status/games_status_info/[games_status_id].tsx b/apps/native/app/(app)/(authorized)/games_status/games_status_info/[games_status_id].tsx new file mode 100644 index 0000000..d78e1b1 --- /dev/null +++ b/apps/native/app/(app)/(authorized)/games_status/games_status_info/[games_status_id].tsx @@ -0,0 +1,37 @@ +import { Stack, useLocalSearchParams } from "expo-router"; + +import { BaseScreenLayout } from "@/modules/layouts/base_screen_layout/base_screen_layout"; +import { GoBackHeader } from "@/modules/layouts/go_back_header/go_back_header"; +import { useUserGameStatus } from "@/modules/screens/user_game_status/use_user_game_status/use_user_game_status"; +import { UserGameStatusScreen } from "@/modules/screens/user_game_status/user_game_status_screen"; + +const GamesStatusInfo = () => { + const { games_status_id, oauth_id } = useLocalSearchParams<{ + games_status_id: string; + oauth_id: string; + }>(); + const userGameStatusQuery = useUserGameStatus({ + gameStatusId: games_status_id, + oauthId: oauth_id, + }); + return ( + + ( + + ), + }} + /> + + + ); +}; + +export default GamesStatusInfo; diff --git a/apps/native/app/(app)/(tabs)/(authorized)/games/_layout.tsx b/apps/native/app/(app)/(tabs)/(authorized)/games/_layout.tsx index e83132c..e82a475 100644 --- a/apps/native/app/(app)/(tabs)/(authorized)/games/_layout.tsx +++ b/apps/native/app/(app)/(tabs)/(authorized)/games/_layout.tsx @@ -34,12 +34,7 @@ const GamesLayout = () => { header: () => , }} /> - , - }} - /> + { - return ( - - - - ); -}; - -export default GamesStatusInfo; diff --git a/apps/native/app/(app)/(tabs)/home.tsx b/apps/native/app/(app)/(tabs)/home.tsx index dc0145f..7c8d272 100644 --- a/apps/native/app/(app)/(tabs)/home.tsx +++ b/apps/native/app/(app)/(tabs)/home.tsx @@ -1,5 +1,5 @@ -import { BaseScreenLayout } from "../../../modules/layouts/base_screen_layout/base_screen_layout"; -import { HomeScreen } from "../../../modules/screens/homepage/home_screen"; +import { BaseScreenLayout } from "@/modules/layouts/base_screen_layout/base_screen_layout"; +import { HomeScreen } from "@/modules/screens/homepage/home_screen"; export default function Page() { return ( diff --git a/apps/native/app/_layout.tsx b/apps/native/app/_layout.tsx index ec1f906..512d144 100644 --- a/apps/native/app/_layout.tsx +++ b/apps/native/app/_layout.tsx @@ -65,6 +65,10 @@ const RootLayout = () => { name="(app)/search" options={{ headerShown: false }} /> + !{*A|ozDLH0<1kknCVRX{;RW@=H8 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

_x@+0%~0C1&PM$%@(@ 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 `nQjGsc$PiNTco(?q z@Y5!Z?D%ybuSgX&c&>{@w-|_}S6w##JYB9 zH6?9LsF$-(k1nD^mVBL>t*xVbg*c1VUJfcZ!H)-6MT4A;w#LE*KsuoSbE*+2x`!4X z2%sHV{bgKdP4+XPbDb2F6R&aWl($KBA0~&!K zlzXiQ|E437Z|G7_?5*FkRU|`xS8rf$3DiL{Nvr1sTX=(yb+d|F%fAY&+}JC9C8Ovp!%U`j13D8Ui%pLd%M0V(eQnkl*B`V#kydN={Q+~8BJtHZHM3WzGcw<>#Cq;$+NE#W4QuQnmCn)Mu$_Q`Bb+( zg3g_ETs3Y32s+(UXkllS2-l6{=I^NX+h{HSc~2zd?1qB&R}+Tw;yFKAx_(%eT3FmS zUi^u_Zn9xEq*XlE>LJY3Emb9RIQM&fdV^_tYacR~ z5)F-6jXWjG70U;-X?<>(Zc zm){yK;55ZBCXQ6vxjx#D+Vi|`P|f6Ub@k^LPYQWNsKHhD( zx;k}8A;{FvsaYk11jnm<>kZkS8pVT~OGC1_qrEi?Kl@#jUG0BX_Z@S` zojGecabmAqhbmm@Bs~_v4$&K_T2D#X(;eOSa0#UU-Dn9y_cCiU6zdR)dkGO__48my zlFxsz3F&SAE=7s$E0+Yk-S+!o+2~}>|4D}bQs+I6&f{qQ42b3h8=dUccqC4;^$X>5 z+8Rsc2NP4|X~@Bf1$))#Qp2mDFk~T(*_t7m?wlX>t8;{KB!0`ue4&@y`4-?icr6vA zj8#YV-b>3X)fy*Uo6ycnZ-@uqkW|dlqz3RR02_siZI0SWUQx<&tU7%V@7t#I^ zHQeEvPP2oFp*@p4ZbXSxLV0^M|Mk&b$_@38oju=%R&yhI5oSwo#(+HR0>IQj9VQtI>Arn|sVPNi;K#F>m49wJBY&Y)@y|bQhqR#0b&s;!PT+%cIFNrmz0mhT3V!x=52W34@@W(~F;wa)gf(*f&z#udw2WfjCrwDl9OD4q`SdDSF96Y& zKz}1}Cjj0P5r-hG)5}pe58zyI%Kz)ZY*2IIWzbys|M5f{G(*7~PRN)2LjV$@#5UQc zaT#y^4p4Y<3l9&Vn#Xj5{Lkq{He$$>dJy^zlj+uS6ex_&qWm5{a`>*Q0De{A5P&^p-D|3PF{ zx6YvcJY}eXjq$&Xwb5WwsusR30y^XXv;&U7fc}19mrk#$b@wGi3Qg1H6GSfrZW!}F zPruv*@)4ymXiK^Ov!$7X&f}iyf9eUlIRX=Ojat?1fR&@b@V`oy04do&hnsboo26}C z`}wz)0d>ngua${7_b=TOTll8NM6iuMfz1A+D{NU_>~X&54z7b5%d0<|bT9gAy?(x_ zX!N-pjgx+of8Tsh4{B%KbLt&KeLv3JF8pB%^oi7{UPdaKztjPM64a6~_D2$+&xGka zzxbt3uw*C}Jjor7#p;BTzwXI^UZa7vKf3f!ozp1)jz~E?JxubguCW{N?%$eiJqsE+ zReL{zCSA~y_HWMhRAn6gc%dYk>B-Fpu;P3tB95=rIe7C8lK4Lh8=Ry z0i$+!$>wr{u=C96*85#pPCKFcxj7rT=54#s0=AVY71yxZR$+-eN1sYOvyWUJp85yA z{D`c>&RR5diVX^Z#>nP43nmE$D$uSf@toS7n&G|$w%T@H)3L5$((;BIZNPeT`1pW? z#_2s#pHn+sRIIC9+B)qyl`41x!TBOSb4Odp+tB}ut zawvLgDs6DT3^4l!j}1aCrY&uM?o?|;lO&7D@XnrN63pFN>ejgqd4C^rkWc# z=rd5s%yhfz9A*i}&&tD>(`I$8MZ;k1-tU!w?{yN#LJZk7B=`4jY_IDl(X4Zv8?R%iMMG|+hW|}?cpkG!$cEMR z6w{mJNYJGYD)1+M?7TD7-EgK$Ouj-5n_?o)mqk>&Y8de*h`d|pGBocCe-($WqbJR7 zkk%p~E3mXMWgIqD4r_|_%|q-6gsrTY{AC<01ed`|Uzt&{-DYE}R&pX3H+0`kTx+yWakUK;ako`jV%|N#qZ`SQ= z&dw6xEY`P?TL0RuyzJ@wtNAS6O6D{D;su#rp!?;x^7xNCz;?dTG&APK2!ASb9(9O* zWU2i)g5Y!k3E!bs1?m8cX$1yOEhmEMm0o8dXE`T4Xn9|FpF$m)H1L^V0TZwK`KJBh{WX3iFuW)2?C$8vc;sCXw!WsRa!dI>S+pb zbk)r7y7O~597h881GN}w=RMDgXIALFd3|DmssyX57H>;SrkXub&fP+*RXSzpjPz{< zs%Uf>39@K@sKvJZhTB!_k%ZaaM9Ta1ekwW0e+YLWzb$IdZ{ZdYM~-Y?9j#{ zE6_OUcG6*7i_GIa3cGb37%PFt`Efwj@jk8!By`jwpz;pIBqffKb!$cVKq_w)LtnZ1>6be^T2_iNJp68GC?k7}Qcu%f zpI!t(*5dt$h^z^T*+D#qjgaALzZRKI^`HP!|KsrBJqX#kMlhx*5!_Qxo13UbScZ0e zOZ|3rXRpCaH(0kl`3QGlg}t}J*LQYX(cxL}>JOj!JkM%uqO7DuTfNcQl`kEE)eJ`k;(Adq6ngnKd7CNgrqzHGSH+0Z$a0 zi?QFO0Tak4q=wP=en&LC&!4MT6rcDmOi%02iZGf~l(5S!$*zaRG@?s<1jlj2`(G`sd2kr(C&_132i{*7IhV9jbLHfs4m$7! zcR#efke0f>ox){4;JDc?ZMbC1BsjXC;hqq~WS>GOL5XzttDHL2&_HiL1|g}u4KRbw~1zAg;;<6sxN`Dj-D+FrxQ*7AWe ziXd}v#$xKLkhpk|4hS6zeiZIiAAJfSG`c;pjuUWHHC+!+3Srp8CPZBN@SD6{Pz0-F zEG{B37?}za=Ju5BD)(7f%M2gj%eb7yz(ZJVc~^&6_rSK+p4VPw#TXwx^G-N(Q;)~J z?Uy^IC@%A(r6f*Te?CX#U6sn3>z}6kEp?*gvF9?k4DM=}Rbje^D1>7*fKLZj@B`mF zP4_3^i5%7Yy-)Y4RMj|K9VlTV)*TY!Tdm3FhsDX`d=Zkphe9__gTmYeu^IBbJJVs$ zdwG}zkM{k~OH>P(mMs)ChldhQuWxNeB_37fNe2y};mPuO3tr^&INw|kH;Xhes!g3n zHVT+cVVv^W6d!X>FAPa=PfCs3nnQRv>DpsFJoxG=e?x8Q8<#`Kg8RzUExhvu>d-mD zmeGzfBc>nSZMxp*=a^8(CL}4XKtfSHw=-|IWOrSlie&~sl9xaNPR;)S#8yKsLk{g& z2H$Wwn!D<_f>APD+7;a36n3GqB z-GCJ#ybe0WY&!2ra|^llY;OpnCz|~8!(nVN#yaPCzFwy|funuqh(z-&ldil$ht2p` zdPW2W3N70h=7!K<29pUIR-{AU3lKdNM)-JtHGr>9Ur!AP$rclz2CZKp-e-^jT}DZ< zxKuEH=Hs<$*e}a}UaeZE?(2PEKH0j?=7`!^xRN90$3WZ8bzs7~c2i0ERtb(Ni?k@? z^JLW6&k|Z*F?;NtP<6I~nKP*Md#>J9W>wAL2(q92=%~}7^eu;LPYX%;S{&9#@ty+j zifg-ni`Ug4ph4C#Z*R-TpbEr6F3VR2STIjC#qCS6K|iY<07;cK`V(1B!l+NBLoVTo zLo1TpqP^1&=IgEpBRA{%x%RMmt)w#sdboqD>NDpOt5Twxd}*I6KWkGQ!YL^69HhuClmicK`Coa=Nx4k?tii_eW+UQ?_w)a<@u?wdYJJ zC3NI-N0b&yY-=|d929*le#?Fc4ql9gtLuU{oA^0SeTVTmghgrpFzx`*aZ5Y^thF7( zGPL4uauiHJd0TGGyXHbI)d=&^5BDr2<%mZ2nwLGq99e18pD1zs_z@?#EaVZ|BA-3c z*jue*zHn*tpu|_ukVUK9y23|EF1{k*tkU}uofkHZ<@9^5jRaKM zTIt_FqG;~F9xi4H@SF;{E{7~87D%?k>9TtjV=Alr(r*_3uynK`{)xmnO6r?rl}h4~ znd*_a(Dc(ZUcDhOwv<$(pwsDD7C?TUSZ)FmNT`Fg%E3J}lTD45n$M9Yz*DTU1I}B9 zjXhN8HP{(F`%783c4N4e?#gqTS=LA#rIxdg+p7vDe|Gg2L@c*IxZ@{fJ7@+??Ed9A z0~CV!-GXQ6omSbU>>Ji6n6-t{T`b3Ps{ekfzBw@PR|R$PK51Or2?_l9)DW2e{euYtC(Ew z`Iwd~ut4+u>fL62B4u$kS((5ypO*2S$ShE|uv|g+(Xdsu?WdvHsd@D57i$d;0b~S@ zys|(rlITgRq)3UGyd))lMt9HnMx5F1weg>L&!1DN%(h?kTYF>?kLC+|g1M3*;fR*G zwQCohNaYjMt?tm|OMdXQO}Od!_>Pz(exGi+40!JaR4V|BobHEF3kk)0PRQlHuA`w9 z>@}4NstONP<_b5D4B5K!;5-~f7oZ-t@LLLg6gxX1(f8eBe#{d@W0LJ37bBC!@)%y2 zu0_RE`PO3Jou)84K1)Yk>5(bI+_CSp0^Sz^lD5)}{Ji@PDsl0*J09G(j}MoTyS@}7 zB^OaAZjr%x?ro8TJyGio_yAvkdgQF|18*)6ng?QZ)xe7i1*@r#syo6Sv)&XVYqzJm zHe3+rKQ;~>7uP^z3n;0kFn0g}pMeBiQBT!){u?hXOS~s@tE|IqLTYx6cWZ4;A~1uM z!i+AeUcqzwP0Uv|BWPW>U{2abu57&1JF_gs=8TGAcGa4Nzt-EY4Ja%Vy<4h7GYE+k zp_8joEoQ9VKG0DdW5kUWC+NtmKvg@GedW$&wiSWaoTb z1Xs;LjAn7skmp4>b?~^$VX%thu!NxrWe045#|)gCj6-@S~QaHFCwwt6?ygP9!@I_`{(w;oQv%Z96^Hb`zAnsArto;e9@M=GTH zJSyQ2k6(szwSb^-PWfqp9DmC4px~@$U+^BR!1l^JT&8eDop6irn;!bWVC4Pk-QNL? z`58}W8)x?{6AAR57!*_)g_7Fp8&#~|9G3LGka)Y(!b?3niralZ#YZ4tfIgTslt@p6 znnOmzI=^V4cCx0al|D(eWScy7GAaWC=`jF zN0+u9L0W@W=N)SSh4*-`$^Dv?@qbJDo&k%<%#Y&ki&~wlY)D6orNa0;>z$8^=}ObG zy4OqTdxKc$`0+?IAS%wBDZHbqyyUVobZ^>S)wWlqX7%du4299;JqSduyNj@Os9@dh`f5RZ09fERdEogSSNdJ;{!~;10hiZ-AQ>O7Y)!*j zXMh5dtHy7#@+nU${8?Ishpe)pXcVfEZD-E3= zxyR#W%tmu;I{BxAN;wCznI|X@izgPjf zoLVp}RGESCUFwT0BE_GPzm`|BRXhK@o7EfQ#Dr|ml>HCYQuO5YMELSQi@RD$ACFSLKiWNoYf8tQJ zO-eB)8#8xhRtk6pG)H`4V#eAcIE&NS4&kG^j5ljNrqk&Tq3(Tp2w~Cc&PMQy9j&*K z4t%ik5LKivo~e8}zoTEeT1TFxN96apV6VE7k#j^e;=2$hb=@!9-ib^X2N7a7(WSxE zeotdty%5av>!0)rGg5>v6*l&DJvQOz`@%E0#HYXf*6Y!u6_`e}ZcxRHu)!K$?PQaP z738Z5k6j=Zf~K#&7`y`|s0o4rjJO@GINh|F-;#1t?Q+Gzw#j>BStR-r$%?Giowdw! z4!ajkTv619TRFEckqMHj=v~IdE#)OsH!Uei;Ka={2(iDNxksB^(}T|mkl1B1Qs4&;Ri+>>Aa>d+Du+M%sH6~BR*d=5nE$Gn9JmvSq8?qQH9$K2%c^P~FnjSm% z_2ZqH?p740h$erBT98OiT)TNE#stUuT}TRRux$bz&bK$q>@0OE!ag|fYtqscl;;@j zMx>~=Eb_8LC%&UaRBhW-OsA&4TMYyw+3sT#Igwt)>vb%?zvrJ)@nk`LH6A#ixRV19 zJ+1Gd4hiVM45b!twk6&+!IQ4?O5*zhu@130Y_sGOvL3m4zykC}Cf;Q)j$w6@P3p_~ zfV<1Y#@QE@&No;sz-8j4PDjfXUEP)_Iq+i@1~hjE^3|rDP{{~8+_>&fbAL!~d|;nm z=s3Gq_(Fwkq#26W+#wclN=6}WPGYi{yEu(yUDon*I$cR;mfRjSo{C;y-)H{rS2KvH zVkkEKbX?-P+~Qy=aRQA$wH#tB_E5eMWH)L9p?5_av%SyrO2+n>xE%ab%ye!YauXKO zI>nbS-7uDENAb7V;WU_6hL>0B7k^Y^RlB3MkYoqdKKjKiM~AvNHM^QX`&cIsb>uv9 z+eFnU$gzi0htt|$9?-n*&5~jQhXdv_1H%nB8RLN{hk%?)jE1Lv+g3giU?A#*` z+aKo=4L%GP);TX(%TALzZ{zBAy?{L5EUn@zhpaW-?DMClC)=&qgg1f1X~2Lb`SI7P z0}*MgmqESS33RR3DkQ?vt2i`$tKZa)DDfJlmIvn0ZhOi(-f9&j?*2twxRjO0F{%;p zBs=2vH zI7=QeXIySkYcDO0?I1JtZi1oar_q3rH+q2P>#sKgrP*x&bpGM3Oq)oQxK>s1%3L4- z+DyoC3Fgkuu$x5O?tO4n=s55s#DFY2Be|*z{-Ug?$p*D0qxg%{)*+Uav5dSAnP9>% z-TE@HlCFrQy(1!pl?U-oRP@v49eA}t;bj*q^_Wby>JQYQ!tgAA*CcIqdYvoyKq*(R z`DF*78aRXlnw{ZJVQbZP&I8b^&ZMA_ufKC@s$o9Oc5v3043|EwEH5ct&1-IF%z;Ue zw%~#i*LC4_d&T^#} zJzH75iskb;lNwv@aPmMhEm>>c@@XVh#Rhmcoz2xeEi~See>+AJN#S~Rj)Yr5a$U7; zmqX#DM`prjA8CJ5a6qI9oJWc5Fh%kYkC78N0}TEcw~k7@FXS0TEw5BS>J+&5Nx0%O zlG1gHzLbB^)z)?@qg6*3fjanxA0BI!_5MjBNwHTDzO$n9*%lXEpHx6e6N@58G~wgcKZSdefsSkbh?6@rMk_*##__H%$_)Bb7HbcioU!hegslX#_@=uOl>HT-rX&e1~LzP6;N}8B) zm;4z8dtMNoz3Le(k(2*yH&8_{zGPvRiqF-zzzzhlTyjAPMpf%cEtn3dRpdPniRAaL|07dqGr6hDA}bp_<77hEcQu&6I9c>*bV4e#1!cjdl{92 z`$>`h4jzb0UlG%8H}q3u&^bGe*7td0=V=F*K%_}z5}6`uyG^{idfD&?5jcJZRT*<< z4SQ{cAZYb8>ysjv}vE;3CxsQG3Uq69ZpTy)~5ls zhAK6YN$|QrwLgG5DM?FeUBKz=g%s{kR&TGp!eQGh_8boS0u(2WY{$;W5+x>s@20}y z>Z``x;GEc*&3#Hl!sv~fz?)^Rhnr;Of7}avs7D1N3wK2$KDk!ltDI06;XI2xqJ)YB zhYq48zE*biGOxogZb<9N5dU+BY9X_OeI5&l@W}7RRzTaq0UPMys9C7ee(aG(*eHZ% zUFJvzaz-Sj3xpbw=*J$HdUsXL(U{~fh#*&($cmlhi`|=Y`!!v&%t{w{Oajsj7eP4O626RkpIK)ShF4J|I3ohnFi>^PZ{@V|gb%Ht11LtuQXt z`QQse!pRkOI2$F8-sE(gUXwonY%reYJ(_fO*Ewv>Ak)?Kx`0fm1k}kAiS^tw)L6i) zo-}$B6Z8**N4T@B)x;}EpFIvpOPXaJzgxyysp)j}%$a8jUr0fPjN!M-k~NULQPt?$ z?X{jSM|z#v*Jqo8@mMYo!c%cXHZOc{IC}({{jMMbhrOWA=8>U0 z@4t@Wm=A0<93gLJSgU1XW#4JSO?=(*0Wdb}h>qs)ZAByO^-ei+!GZYR>Ur0R1NCSx zTdK?06I?(lFGC3`4#iKIYa6-CEpASXr`WUQGINWdmxt`qVE1-@nX>EfdjIu62eZy^ zhoc~o5X-Lh#h)h+zA!zL*R*wKsS#%}J4+y6Na8&n!c~^q73;}Wzp80M=WWVk&vM>% zj{Y2YZxVQ$S?EzG(yg)4A!6sKlNUB(zKm$H^{-5be!xC=^~>|{qIYakl20SCHFjC= z){NdoEVAPARbGAZ=xHIYDVT)`ctJ`drdpUy z7YAKs3BJC$BPE-+!p?MMd@EtKJ{QGlowGbc@gTPTpeAZs{HxG8WKVFkL-zi1-j%uC zP0OG?3%BQAJB5Y6JF=8UXira7=s8-{R?D>fDzPZBT+@B3Up$v=^79#&rP_y4-qNwv za%U&L?-P482+}2V6r;0y8G=7y>@k{u!cTAXx8UYWMLqI4ErCwA+2HB4bfu%`9v5yN zIc1X$PUw04kQ(4)YCKpo_H*{mPnu(i@@@7?obaSwEQy#U9b3Sl!CN1~VLmw}y1gnm zGOv|q2n9SJqUGtG-IMH5{8_FCMn5J}#zyBZP+YVQC`Xt2p$2$cErKCr1HU0Cj zgTia{g?8Mo0!8pcEt%glDE2!8w>*fh$v3Xh*Ul2Z`_VC~Y_}{f^G(<;nvEY-rSaw;a5T#DG@?TXk1lTK{)4yt?c6kxMAFeVAE z1lG0**XrPgPJFLn4UHoRVI+KesB~&f$cC!HS>abw*``~!P1T?T0&mM5%D<3EQ?ilkrOkGapiT}FWvtYObb81)k!Zv8N+i{0PKF3((1 z2)DGBD~=tp;mpR4;*s`HyVGFGdmluoUdmy7w_Am6m--%BCCzg)79 z5nH$@bIoi>YxUJo8C{KK1eQ|aG(kZwkp2{(^}+tHu3VOpw*^_zEPVqmUS*@5yw9yU znUkU->9{;f!m*g1p$`0JXd_Wf{M-a(MEA5c9n9I{B>x^`z3v0A&y6Pz+XNpYhIvQUdv*Jt%gmmaSfR-$ln|O-27L4RRwpp z?#l9xTyaI! znau~UaCzG5?k%eW0}-dOS;ahaO`!VavA&lou6EW4TZ*ad_QpM5Inlw1z>MLT-M00< z#fu!~0oxI8+}e>kCKzt(5tE_HI=J8xhxxR2@%9bGtUU=gbrgb~zch9;-z6WxAc1YX zvzyZR1G%GHyduZzky*@)kMoPxXNzd?DCF8f5N4LMqkV2|^tGU0Bbp~Wk!`zd&YyLF zNS99a@nxi%RyJk>hJ;}V%k&;g%$6dQ+4zOi53=|}wp-Ed6%k?y_<&v2i?Ow=ctnJS zE(pvwiVEvr4rBZmgG<*Sy-pAO;I-T^$*0Q5KGdJ@?4ALc%+;a&Xd4cT#6&Iyxh!}U zD;5`7SU;iNV#ZKm-Qpni2v2N3A^nyaW&ZSCB|D;QeV7r~(V5^sy#TW>RRvYDe58yc z?vsTzT_!X4Yb&{iT>P0GXN z^fk8fsa(RRtgN&uCGbbe<6*6|+#g`F%UdnT1mv)K8lv5?hAG?V@VV>OIWGMI6*Vcb zh;VgDH?4VMa(?qIG<#)f3|Iao9{Mng&2GER-)S7~M&u|wmo!A*3+)BOcKA?P$s`KT zS)kJq_%4sUAf-$droO%`lQy*p`qshKWR(Wx8OP_A9D#w|{V#3@ODVr8Dk}TQ5~10f zWzw#0N0c>Zr?Mm~9Qo1G1T9}^=s#4|@xn7@(oG^wDHAVKM_pnsPY+BPA)Tg_kpIRf zW9WMCq$ZdChugwZm#QK#&Xpi)o)&*@#Sw^QyZv#fdT4QYzP|cNriytzle)y|ozRD- zch(OY;r%=bRgW9w6he^13WNnWa_&GM!5`IEI}v(HJdvOE>XT{OPd((Sh%4kb-)ukv zae408&ir=R?F`76M1|dGn5bKV{>yZ2)tQb0`hc%DXQ#|@WivG26cG9asq!}EvDo}` z9*pGLe1RX_CWI3WYo^N zNmQz5+#@A}{%NwNAKmPuPe&0%ig7|S+>m$;-4)}0`U_a2W_{`*f4MxtoTm;OIfeOu z_dSDDe9*EePk)b;2J$PHn}H9j6DippCpXw9x3~O9rz=x_)Rp*a6d2+M(7h5y~#MslhWS$g92OP;4wmcIM%7KXY53-2l z|4yn@6z%qU{Op0j|Gix6egn~p{>*!%y#HPYp%bFeYMQUO29+|lI=&9E@`UN&W82JS zw3vIzQzyUKIS`OX_rK%;Vlm1fE}Axw1L)1&5F!8UgxY|@4_tF9=~ZvtknsN(#ZwwV z6ZLI8+Dw`wa&ihQ)IoaM@;_yyYrSHIPc<~hHD7Oa8~0egV%!B1n;R!$+}$sD3?NcM z72Wc{@4mPiYX3{Ry4H0tK_+UjdA*%a@JRefkGn&7>v>*IStgu7>mNuarF%D0sb9IVPXaPj)M4ZNb zZFZ4e)y>*9zgP7)|JDecZYcVqNOelb7pM+aK=LzDCo3`c-k(_2$NZbvS9UiLDKR#- zqXCUXqkrEdBzf#ULXKGu4{(zR<8BtO&>Qe?^g%(RIwvPqWrd&**kqv&8SPu#M^~u8 zA0J3Z#WKMQ5ndWW+xVL=-few~CnK&<(Mql%yKbDV>(c*^8lCL7kX>EkDl`|EFq{%F z&Sa20f8oc(<9R@he<(KIhW202Rq~!CJ;Xk}3HP_%)1aHDPh%7*ioV5&M;vtPKO`@A z3O`-5zQ08tn5kD0*(rZ^B#q0`;OiSbx!@Fby$`Z43;$gP)8WvaA2eb~K$4H6 z&l98cPZkKp4P**0d^Ks`=5coSJ+?k4!~--Gauu3Dc1@xd?)?9lC)ox>3SuC~*}dsM zH+xriB2rGx&OgQF#5motU>C0#0P08kZ}lTL)h7E(#er~gWej^b+~3sHr5!z{^nXk! z-UicQ%pI|UT(z+8f7{n>gFgbVD=JzfX=W)374>Q3ItY>d?F$cMx{eLt$ zdO*jbrw#SdC{F`Yt$(Y}1&=kPB24;3%c14}@Aj@Q1V|AO2ND09vq3%R9GPQb1b&L=Y2Ht7!6n3;q{Ec4^n6OSH{@T;1P7;;rk? z&|~SQ^1seWB>zY1p_ZTbxa3#hAb>TglNAD{<>iHPH2=aMGY%Ob&5c;_^`VEJao&wS zBmW`9booCOp!cTAGTFP7R>zR;syhWwXk2n~PuvO^YL zGjJBip7tM7Ad<9+>}um;CZ{0#H?44L6j1!b>rV6#gZlrou36Q6P99%8#ku1QBG4x7 zpU#zhVOdHpPnnZA7^fUEbSET+J|sGwbjaHGu|E72zB@7fq$J)RV`Lg zk?woq=On3>T~r*~aLMjj{!nFWj;F@3=ICI`M}($pNi2t)Tzt;^!0->!nHk|!Q{BVV zmbstPH0E()JlxtMc|bsCwMC(Nx|`q%^;HE_NRc$7;f<4I=Y2xl{?#x;w0p4T7g1X& znJ`TKdE^A9@{7~!uR=!#2mPh3U+~IFsS2O#{6UPzu<>(EL`=mfGoK3B!RiEEW;0io<7Xk`YR#9i5Pf2)QyMd~ z^f{bE?(>v)n`R!o^5Wb=;$+3x+3NT^=9W$0UD5T>)r=h->M__^RW_hFd{Nyf!Nk<} zWqf*5T?XZ)^2bD>rrf|+JODdGFGa>7+8&+qxm_hJJ+oQacyjhE;rRwhj|3=0AJ%t# z!OdUd8^v&-`{1cW5M$-eo=bkQsA#?0JRUj`_24VfkDRX-!Woe|PnyntXymWi(s*ue zug{hNIah~I4OY62<_)o`CtCcHT)-}EQnI4sxYtpVfK_@LX^^N&#T55r` zTy)O#-Jh);15w(U)2vE$;?XG*DsEbHnAVu)0hvz4bp;lqT-xOKSeV1zFZ^zrH2vzR zke;SZxw~|mKJCj(%ZoCpr@n2RHE48yT3M`{xwjYp8gbAKz&-azYUynt;~{jz1n$<# z^c87v*hfE=2qaFEkIC=cyTj!K&&uJDm`_a^g&(l+VUE(xhc0HBxE}c|z0J(n&~=s` z)*>5LMwKT^*G!D^Qox0tKj+A=nQy5U4f$_0Kn-GvQ+s>h?65mzthcYW7^c@fgOC#s zBtWGU$9jIK{ta46YW^Q#-vQNB*0ueC(a|DAJ`%AE`s{gqARl zgGh~tfYeCuAcRf;rAn7hsM3W50t5&U0{;nE0O$SwwU{+56Yjm|oPG9w_CC+EZ|W%d zzZW5$trNfXPiD@=3;C)AF;4WxZ7#^1TFGCeHt&BRL*%^n72nY{mWayzdNuf7#cWpp zX{N3jbWem;{LeBC>aEVOULRG1$F8ujtW0Tdvy%V z{inWZYhKgP-VZ|1i`4T>2oma!&b~~I*p@wm6~0% z{Wh+e(p?W(5rJ*j@j1J(4_*+LZZ&w*e!H)rxG^p^8OO2+Qdy0}2AZw*);TSc9Y$}R zi@X^))J{rx9COY6Q;WjQoZU&6Dpu$ziDr2-GMh*(=n)7ZpsROaYKcS~HC#v<#Gja! zj`Th}Yi>xj{VQNKpIES*xtyfxzMk&8qcRP|zzZ~<_+!RQJ8TyP>wPAuKThg48Q*M<34YewGN)=btk0*;{2QXU6@o-V&5$~!G{b`@jtGTs&dk?gPeXXWIke$smC&Vf zlU?j9>+G(oh$9$kn8$F!;P#?)+2BsnJ#Eh8B{WXU$;s)&${A`fu{^^AXfs{KtfgdM z+>m|2r+R7Ws7nF;m%Z+O!80WRo>fm1UgDHbEUnvvQ0o`0wU?psnhx4UO*fMsw!8!e7>$!^|C zd&h>GH@c71_4uCYdx_`IIdX5zg$P7@;LR46dUIAPn7sbGF9X$YEP)CTGLD>*`D=KV z#ge{!*@GNc75choMmU^R2iw)|ICg>7Vfpf0YncXYO+58AIB!kWEpJ}~(Fn5gQIahV zp7pN2$Zo+y>L%TO3kN*_qVm8x-k_|bzp*zov~2O5h()4V8`lQ!&9zCk1yXE*5D~Nb zUnB5Ty--8#!nu#j2IDJ^1&?GL@oje@Vge%}B~W*j%`XOrEXca&v9NwL zS=}yRvuMfNWGS&14-niqMs$a6sRuKL#29N<#gxp`j+Pbb1)sU3hhw%_C>utvI5Tny z3bMipCs9sTjZEYll)$uyA!5RsrN3YNtfmv)tbER^Q1GI`>pNAG4XCtGyp7%rY!$Wn zw|qA{q5B_pKGwm8MS45oW5nj|;Snd!mnXTr%L`@yBg2=srMZEX+8ip*kX7qhm&M?r z*}0&!5@{2SZrG+bvAZ=xNyEoNeVS`uPvYalp7W+pWyn<$xj`O|3_KYtU5&UzT|PFE z;8_J5kfpQh6JiTCzY=N}=haWbO0i2i7N}E2%*A(J1Vu!rtg9O#x9YGTA@?M!Nv@t% zB;>2yEFmf1%=j9U#Xl@8gcKW+w%;-%ZbL>vE?F}f>4l!Cyt!eX?qca-8T2b7lLtT# zW5m2ixTnbQ3`-FV3L1gKsBBJXzigEBV8J#+1N!1&KS@+)w{{L#pIK<_m15s~Ph9$v z!g)pX8I}F>{A}2JH!o+jSnNNY4clvY6VO>#R?g)<1)q{BfifzgZ@V`$eeabYVnGZ_stgxNQK&XXoctH@BB;cMt zlEp^gqK#C>AFz#?!8Qa{yPfM>7Z~zw5{qF01p?9GW<8xIQI}1-FQjMnYV?@(+Y7op zqd=*QkY)d$iaQ5hu6NnptG>m*pj;{GU~KpYw4c0JE4dQkqL^{^$u5rl=XDRd&azh= z8BPZazZ~7`-kyCc(?E_KtmpwrS(Tame3mEO5YD{;?M^rU%-^nYGJLW;yEfOGRR-~D z%k93&GWo(8t+gYrl%g{wQ(iEN)rvwz`BL@HLLU z*-P5*Hd(%Z+3zdKxow8eX`3nr)x0&>+)17eyj$_9OJ3urJ*b8!B%BdoSwur~EzN2n zJ2dP9X4v{#MAhtv%6DhJ;&+Q_Q2!~rDMvrcl0{#R<5=R2b-;&6M$0U$VzZ{mt5@x? z&-%+pGR=5C+eMEUE-W*WP|OXeV_KR`uXF_S?mt7QFP-TcF$*r1_bjI$uv^33#;{cp zQEAoV^_A}y-jYI*oLZWhM^ZDQE@qVJG;t;;sA_1+y8EbWHoke^*~>9+(>Y?M69uZ< zA|CyjVt*9bg_lX?tWbf1Ma+AS?b*v^tcB$%-r=B{021DF_j8qWFOlA**5=X=jBaEy zH9wK4`)tUiP3zg{UY&XJE2S(68I(q6We zfHGsfv18VIdWo1aXz?dze%pk~Pv->PGoo&32ZkidwB#i^qV;24%@`*-IAsuE9h1;^ zIXFxolyc<0@IP@;^(sx+IsA(?A(0VV9o@-4D=-DUp3}!Ow=AGgsLGXu zCtMOU2x4Tbk>F6qYW(o*K=?^9)H#ThdWNlbBsb3V-)SwFUmt)U;;pV$nC{814 zR^ISkqru1IrnIUGcA4OQ(a`1!(@2lBu#vSL#toZ-g_urx8DrctbkB0ZNYJS$BNFni z`1fA=p&1(kyWSi=yDWz>^IsshDM^W!5U9_2AXPaklmvGx1fdV}w|#jsBc19ZaHCxt zD11hW@x6I%q(iVmjz(x|QwOsd!2>^KPTt(yw84J_ReLGl(ZI!wK11%*PcGVCItStg$aLi@`Ij3HoSSN6tBc<i;?9U-xQqR$AF~V=#oa02_9~C8ERU|i4Ve|$zTmoy zR>W>fD0Ay2M4+{~^c)kQ$+p(FF88@p4cnYy;y}+lW?re&`fNJW{N~Qj^zFlgtpUoO z%93+<5ko_G5Xi66)9Ba#tB>Ma`ZSzxr}aBxx|TgJU^JQbX2^S~qd2ZqdXdSB9d8V(mMV}ESL>Wc0!q%8pJk0maYqW!Wnf%9McFhJKQe5 zj-&pSbE2@ZWFK4z-a@>0gWw%OF4dP+f?RW=t%GbM+-+(-7j5zJo&gdrmKb*rjJx{| z>*o*gwM+{Fb}MkM-*I@!WF`G5EZuA%8=PP$krO`SQomlUWxD=hNEhT#Ea9U-z}uZu z4VDDRBoxqB8Ca`0Gy5PP-sd1{`~xt%gL-Bh#_y4?4ngIPqWvT}u^0=KnQq^g~4 znbI7fJmpba9~dyaeq5z;h{ZDY>XfBiw)=K&DI}jp!xXu>bJUAE%RnWyCA31UYbmUDZpJ2Vzy=XYc%3_(=Y>&q6_qZ6CRuF#$2ZIg_^k))bjc@(Uci(KCGs!nsFt$ z0kU#2Y{pI(U|q|W+k__6+Kd;ujrbY8rY>#7D`6J`mVfEoJrQ4pEI)lGDV&|1I(6!K z?4Y!jV6rBXM4QgYdf5Ov8RIvDIRp6{;!4P;Bzx+zBOFP5pz2Tv5TX4>zeZp4;#m<0 z`<(E+4LDlWanzXJogv$;bw7jges6WtFm1BGx&~O{d*IltNJdrqG=rG|EWW&*+L@bU z?Md_rH|;ltMk->j`!Xkj*i;puaxeH2Zxh({XBOJ0LV7?_$Hg&uN5zTOgGZ9B zVGtH4ki2TD)sRDa9(c*d<+pm}PWfwF_sPAaL%841Jhw!XIUX1OQL^(ev8e3Ea&a>OY8l1{(oiZ$7uleL=%%X-w zysO0rpCs1kNJmZ%bpLuv#vfn;ldNgFb?G9RwfQwoarI?bp6D6Waw#!itBJGQ6Y^%7 zdwJ|ZO5*g*7SrWyZXM0jVNG*-!7l?DXC`ts&Iy(Z^~n^yB?>BO z=yX0h>b6J~{#8igiP7H4^mAPzwwxs70J^#~UoInK%UwX(+X-WC*M^0m$nBr5<{`Y4 zdK8?-wvH35j|HExI(xl-i%|C=+QgsE7US>{%2b9u{E=$xVY)?8I5w1m%n3 zw<8*P)_nrf)zwdXnbZdoZBo4y?rCr%veZv+J&|CObNAs1O`%1e1(x|UT-H1%J*&;G zs)_5qQiDnNg=nYBjI{g#%-k3xh9B$^*9kT+5l4#|=w0;W!*yGVifx6|3A@1|n>4=<$OaU7!r#e!dohJo0OkFu+-akDi&ev4P z9ID`cZ&P&84}s7iWLUqTn#s9tUcbFStvcP%no#%Q)B8K{)U@|fS#2BPx=aEemi6M5 zB;`T4QKW*HuYUj&s;VR6%v}d21tb&h_&?hz^-fn1jN_LR8!!fWvw@zu=ubm%nonWN z^T!jIcwLS;MR_$BxmbT`%VD{3}&z zfx+6bgs|bCGYVQu!Iq*fUz5s-cmp8MBS@0^0l9#wiWamY}=c1?B+b!^rC5cM(x3=&%Yb)`~&~| zmSP+;ip~MF$z^6&iplx9W10DUnNF2c#TMFGi%xa@$MdLQ1FbS@hR*2vSi!A0{WrG#KHxyxU^rAIHtVdu%rB z8O!mdoG+V(CLeRN9{Ds5ry198T43nxh$Bo6ntaN&3oh2ROa9fuj{5j`2$x&JN>3kY zRixma(&8w)(2UnkQCvIoXWUu`M|sIrql;rp zW0c^|%JcMCz&!=yr@ME$4!A)n6K=twb`^y&!LKo>E_l^e5T=YhO-2as*JoPZgHulv`X9)kBgrN%Kcv@7?3SHWdNJ zT6)+n*Qd{;iu<29u~T4Liymu*LDk8c5Kt&QPBU2XQ>y^I%b$kU7(X|}2-~r>DgKS{ z#@QS0JQCSFCDJL~oB4<$zXs~69ppPkj2D}n{ucK|)rGE{qoH5#BGKqLEuzZbiU=|T z5FIyY3`iE9t{Jbx^QgN&=UEyB1xEj|Q`j?^e&&k_PUo+%J-mFq>7D(n+Z(Z;?y@~L z@@trnBc**5=U!9(W!_jQYn8D?EEgEONzSNtY%%!_j*NUYo4St5JTlYY0Z41Yy28J# zf0*W1sk#3d#l2!G^RoinT+6WN(V~dt+iW&%q)P~su0raz{dnal;HSC5V4-H8yu04l;>pq?MEXdv*%*!af#CXL@ z0jLAg_cN2$KJ+hHu0Ifjztr(6ixSZ~YU+NB9YEIcBHiU!uR(~Dq?`;^%x)v95@#t0 z%DCc%q>&FP_bseer>d2zENpmX&NPlmnGr6Jpb#@Kp|^^RgPk*aCZwO%&#|oI&j&cc zAL=)v9_XS8*wd`z4R#e3eFjg*XCmO zpGy=1RY0l&idG;G{>OhlJdw}&5z($qu97DlXV~I@jGA~8zqE=y6D4)br1>a5vH_&6 zJYt4OkFr2UwjQaekN-u%P{bj_);IJ-yeW|>SsL2d7d#}Qj*?+~YTVhB$M%5Egn z#<1q`F%^~ALB}H`1%T7Wn`;X7mJN z!lUJFoLlAae#gK$Qw5u}G{~pRC*>k<*F~K6TS4nwXo32-ii{R#Q=wBm;;8J>0LfHg z$RnUYXr@vhwR|LcD(#h)s7LT_FglT2_xxN~*&{~IXeR8l4ab_M7*Nm`U7}Qv!>kZCv6`M55GGrL2ACnPkm}jk` z!;MQgA~A1;0MmAR#oLVNimi~(x%E^$4wKtBBf0^xmPnQ8DMVQdJFh%Y>k z_AjYwn-<1bl%r)9nyr{BMn}2h%*&r{FnSN#`1MR4D_V`OxhXtVv2FR7I+LfO+Oj5_ z>rAW`B`n4pbKQT{^a!;_y|`QFi@ho7q28){Q>rPppH99AWKq{a39H0mMGxF+C_JM` zx5CX}W@jV{4{6Go2OMKA=q>z&T{&-F)Yh9K?wb4kw%7;yaB8t+O)jxqT=S>~|BQ}J zadvAwfrH|Po%LALp#zG!YL9R8fwPr(qxMiKlIe1HHFEP5V?spSlmtO~mRR?4-bg1( z4!Go{WL=U~Gq+@Zsf|2FUx8m&_`^u-5nLj%Wg<`5@Q>nvCyebY(ZA4%ZrrgMUj8r5 zS=j38<{Z}Ma+~Dz;vL_%%v{e48Ap2>q4)!L?zwn50#JdsFZ(}zqQdT zLF$Mn=l4&^2o>9;&&6x%Vk%H?VFKkv%}cK35~tSPrZ;0%$Fp^LWaMd3E{Rez8YO@6 zXYhk!v}P}+FTJyUlLkQ*0#dsd?kHH|6pG2ENjy@podRPAQ_qLm4WpPCXeYK6po|#$ zVk@GNzFk9jHQ;%1d#~}nnTW(f<<^wEFdk<-ElG1&)#Qm1Oi|-ki=c#G{@!%Cd0V>z4wAJsSCg3U?@`lyI%2Q zgyBRld$15t2QP`XST1wU_l|J%;nlEWj`F_|R*dtZ zTC?rRlm&l?qHTPXx_5e3-7v!0?oXb2pP!#k1(&-Y=xkoKY}WkBXc_h#=4%Aovuw@K zVMk|2nPfUK{jgcKXCUMj+@?uS(eY8fgD?7UC`zZ-V5qu zrm>X`Qqt_ZR3X`4Hy*@d3b|6v#o5|kFPRFHQR=5%Osken249&hXHQ5K_cj|vFaZlN z4HkT4W7^bRy{3Znmi>eD+N5j18pS2yZuvemmo~<-G!^guLFd?CBsXg`oNy|Om&(&=eyS&D4;YFA7-N`QmoNLHI{ zO2L?!*ceX^H|+BLMREgDU_G3@nqSgS%_wbmS|(eF-y%b&q&7=GY!q^afw6pDKV0lJ zajBr(It`?*4!W?QI9LDG9Jw4_JDWWHsbe%KD^C#mlj0$(NZT!iCntBwDOp+9$yR?R ztL{XAvxV0bE9&%Y7w2vb8kbK|2!M?TG!m0<-rw2mPBz`yL~i;MGek~;!}a?J4f8nN zwK}`G=A3-;X76$MGWpMeGAY9a2Clh(bXUU%U865gy{_|DPY0!<@G)k>KrX1CU@F}N zD^|setTs4$9jL*frjNq*V#r)~vS#!GSymR+aGm;Kd`07( zrz@1g3HeP=U$N3+C|;(km%$^hbxp(lUI*I{YpETy`Do^84XU8w^}HdXe5-z%l4P^v zjB4YI+a;`v4lWkY{>L)q#FB%9OL&-IwKifzH9BNco_%e~t-PDz7FNrXYAcUWzVp)U z6Jp%mc3a^+B_)(I4A+Z}>aJ+}zBSvUQyIff@`UB{EnV-CG8Xpgj&X1uCqmPheQ_l9 z<~D>rxsuxOq^i081vQSmt)jcZPkF$c&g&K}JQNMqb$KXQP(sbvy#&r2b#thC*a^nS zt(Q=evVuIpuQ)RWY!-5=H8FbzGVLbbx?NE;m>Ef1zFp(DQz1xf?4zp!WBc5YvoVl9A>%RlFqgOf%H*FdKyUjp-_rWMQ9tX9wIcKJ-q)Yq; zNLa`v;rH}8oa@;#J-Cg$%aD}zsz*&nPj5w}DufmZp^W7#bvv5dK&hOzH~u+5tF663 zM|KE*8xO9{X|+@i7b#vy99;{p-r0Ebeng5mu=Y_I0B*odKe0n>4FUq2sdcELR0SM6 zcycuS`SUd1iIwQ^$Z%Q|RLk322D}2KzmREp#l+FYG@=lVxMObD7tpc|G;x4XsX`S(QIs{ih^i z!O3Bod~26B8~Pgxy2F%Qp}5uh=FVK8Q*-4?YrmqQYHDnVjHGcrwmFBJ)G2HX977bZ z81eKRv{H-^df{YrLG`(+Zx}(vbL=*mrZ8W#vb#TPY$$)R>D%bW`DUDMI??qBKkMT1)c}g-k0`nNu%^5UH5_J3Z@5eEwIbHW~2Y0i&R*87g(GCH`l# z9mcQ+&!0!>B?5lB6Wzw!Fi4|=a%-=mrTQ3;#Ba$ff_Ql^Jm-cjUGQxRnZSIC@2pBf z;OlM~b3eY>V${1`Mk345i1_wjHzj0P6<%I5{dh&tDL6_b^^u`uBzxD2yB)u>WE{z1 z@%nq^N+t;suZU~2N~CxpPJVPb&Z8SQ#DO&=LaVt#`&=t#klmotT0q6ad75v7(Hv7? zz+Ki-+)BlmV3{J{>ReTM+NtF)&p8`7vunMGW^`KY#qu#@KQZsI>QV;pe2T4>?rA;A;i_-bHKQ8!aS=(U6xI+c;XHRuS@B6KUbrcxgy>y;H_HV+$#ZXTEW zX5D;GUDEs8fZY7fvux9}bQ7j(06>FFy+~e%F*S7iZgBS$z}5!(_IaRI3X09of?j|V zvfd!S@`V>gn!a$?Pq1B$h50Ila*Ni#vS&Oi)~?oq%HmhpJnsi9RZ`174%K}0H`1%h z2$P*Ii8B{|>igm27@sRTN>raqFM+2b1D7fJ(XL`Bf?4~KwZdT&4VzJyfdb*8&|PWJ zTCi{hm(K%}?@{2{Xg}Ex{bJ$G9oul3Gc}J*0qf%@wuPOim`ZJ><+)sVa3t9MF zUvDIGh7W^NH!r(%nWLwBwe&K1(6!qSXv4*eT-IN>PiY$pU9w*mjq{V)v0mNQxURV1 z8Ra&o;w>cd=UWfLkvtx6v9%>{_0-s_JvB!^KdxqhZI6ETURTBRc_;Byu@;_~4+}{I z4mUvosqqf}o_m+jps57J+GMs_trb@w!lJMs?=DF2I1F-`8m>ppbKa7Ve$3n*Scei-D53l-iOz>3O zp9htTgc>vA8s#9Lphx4}D5C1I{E!^&hf zuJfL(Q2~NEzw^!hsB@9H&DfvFvT&9bmnDnK6= z%I1TcsAyKMH`JOcPxT|g4gcmQ_(4e5=-|6|NPMRgcq7C>V-pC+yuj)wOt-GJ#!7BY zH?G!}ahi?VHoALf3v2$}Sd1_XHIDOQc^)QvPh$i&P&<9r+EQ+4sVj6`-}R*T5OHB` z+umH(vt9B?%+Fd?)|!o3%F6MN z+!#HPg3&BPE)jgL4F!lV**Fg9v~sp@eEjIArX2=F3gvnIQf#}z2sv(htIbUxC%;p&mGBQZA@NL zuD>iD&DdsPRNk7dVqt7|XPOr>SCC~j6fduY%?ss?U(Gage9pnso-5o;9LR&Pjyxqs zMa1O|&;Q4WP{Om5+}#&evO}-7NVJew1K{UX$p-cP)|iQpV7LFhTUE)YO|Es$28rff zkZ5*H6-~D>!J2iK-nbcXDwl$GutCP zj%@~IFzRd~&r-6GU*FO)%b8}WY4llw1i_N>CK)fE(nnJps*D?C3g2_e}gKWzimAWLkl zrhe>|c^mgt~-L<*}DE1v7EIcXiM=yEI2v>fVa6=S?uRoOq3k!u~ch!dB>rKJ)=PadBkRuh_*HP6j@4TjGda8*Xr( zu!N~BC5RcWF_!fq#khi&7Rs0%M)B4Lp;=zBk5E=tGs;!WL8l=&{dPCRHpX-OjxoVc zjYM2WrOM2SR_zF)oHCK+=8?vy&4xK-;M^3aRMR?43}zi2P6MyFK~QCaLsl;;Z*;5Y zCxlqvQz6Ce+f3yK%BPD(r~&K&p0CnHx3G zjsH30J#@1idfrPNZOBAw0h2)?jV$}J!V`to~hai$n!$ZJtb8jy#_t! zR5E$4z4ZEfY$oz?ds~}#6=qbKytM5Y0UgCl*IMgoti7i*! zj*tN%W5{LE{1?i(0yqVykOyXUQq45qPcT!Y<)d82u@2kAWAQ@UYX;~S^N&%V(tvE*@;-oEonh_%MnGOCx&k6=|uV8 zzN)A=a#Gp<2GEE8ssHZDJPjZ(jAZgm4kFSevv^8M-nK^0Xr9Ne7YIbo#0iKM-AL|E z%Tv`1moZ;DgYS@Jv)%C%(Tmu!+|19EPXB35McRaT#;%nE$ySs&j9&A{pbh#ILLb+h zw7wacV(d3n9BCf+c^eW7(G9v7LF}`z1%4UT0yQfdR-1@i%>@MG)vQuVr4HfB%OgH3 zVUOUHJh`~N7tcujt>)Zv+fYl25PWQs_}9tUcVx$eE^Ch(ad?%8^Y3*90Dc*5h&Q(J z0@7qI1V2m^!^chKI|U%f(Lq4diAqLatD5;6F*GAlp70MKKaRYKh>0AfHfopO!e?6& zHWQS6W+J)l+;}81#~Ccwe+8$cq_Ksj_@60-R~OgGVJZ30g5q@d0Kz~NCa?t4vpk=miwg#!Hq~8yQs3^xepkM*+vyruU+lU^XfEgDU17P*}F!c|MwUlYx z!!&F%Ph;4UlC>41wLj#h4GaPlN`RC3!SNF!*K6f1yeogJjmsw6Z)a0k#QEKh%;JMs z;<#EuvwEw5U)}PE4ED9T77N@nntH~YQs)vzJvo{*&Gpj1>4~SM?MfVc2<&r_k#KTmt06r&l;ty;d}o-!Z)@Now6s`J zCt-IZG3~@a)*zJG<<1%pNY~s0dI}=J zS593HnM<800kk^ZOhEGxsv67ox+BPS zv?E)li#^&ro}Si8pldGB1_{1klHO4&uxU*fKQUT`J@XJzotYmsHh}k94^Fl-m#?re|$5*I5dvP1D^<*`d zu0mERGW8kaRYrV9zADgNYZSW00lzvw$VGAm3_w$2uwG?;kqcJWt&ZSpf*hUF6lCX+m9LEIP8T*;LluBDlsY=wPQh9Y;1u_Z3>u$QTq(LKFHCHX|E6(otr}@&f0q|%0d5aKYr0U6XY*n~0P_%tjE{?F<(Hz^{(`S1-u7g+W#6DuG zvH$CfJmZW5hRS=sp(0TUcM>RL<96|%{q(N<@fN-97xHaGKV)I|^Qhg=`3{QeWFkFf z9^aOxTrtL{9K5IH+IPitYX9!gDIy9aJ$?B`i<;;5!auv}Ps?x_5zDV!fTYYy9EXbB ziRJ}{k%2I7+!2Z>dN@}{!T;5bZ(`E7y4KXp$#~>0ff*uun*bzk1FOtO5L9_z1ZiTE z0rvQ`lccFH()Ls|ZK!E%hV;P5I*WVAyAVQ0G;BcusOX3RBh?3wyjgmc}1SRo)8nsGh>Rd*{IvU1NFe zNK}~WqhLl}UEwhFmreBBgQkR`0)gN7w^Zvr68pvLdWe6>>5bxKI5mEAray6OJY ze%-Qp=~x=3P+vJcEWYNtuUotL0(mXs9HP8aa#x4x#b=M3=BH!pOP6B@%=SOOFucX; zQx@L1Paj9wAu=RyHVkTrbNeZE=op!TB3#3zq=mmQZ+p=z3gb}dhvs%Uaf9W!-QEFX zp#ebJ8m*Ih;;_D*xgPW%P{XQU2>y+-ArA=4L;E7U6i?M$r|8D__bk|x(~{nXuMDVz z%;xcJjxRfKPeMx1t7Q01UVZncK-#k^FLhE=LXE^B-M}Lr=mP*)v){Dc&lhx}*U~y0 zew6ipcF)X&eZqRra7)`Q~-_gG5#6t#N5wNw_rxjDoEVYTV5Qf*hO zo%BnGk!q6k`&|aa|CF9@jsO{wzrh#R^?jxSDT1m!0eR9djc8~Akpjkx59vxj9(X=A z&i(R!)t|HW3)24t(rZdb2#_f67p-2&TxZLr3q?l?29KY69*`|j6eS0mXw1wV?C0R(;WJ1AWoI=zw950!+~1bk7p&4=RW! zR=yhw%ez0%GV5czZzf4b?}0Se|l#`RQL_2R&V17J1E3H0BdvW~*X+e_9 z<$zhm^%+UC_f#m&oBg-|5{;Hqf(^Kv5l(8O|lpCDVn+|rZOvAxl%RJ#} zu+N|RwdI@b@4!&_;Xum?&_shK!#P)TPt~};tH>`B`@3V5dHQg4dIHmtsXlbO$^HJU z1J?V6*MK}^HEsJHTIbA22jkcMcwk(69doqB_uqRL3h`c~^cp+71@9b)2uj%x%K@*V z=Bpe3?r}Z(6fQ zvVZCNQ(s1m(vT(MRerSb6|eeTDap4rewv{ZIJ;C%eIc0kpj`!MY#I0Y{r`HuC{Rc* zKl;;(#I`KN4C4w=PtDfE1>5akK>cNN<0?X!Jh5m^1# zW-KrMlXx%iZg}A~^UJr0@MJ zZE41OLk0M+b#1Hm4K6N37Y0mOQe?naa6Wb*c9?g;IT*QAR3e z-4Aw-YG`aF_W3(khT-ZNNXDXQp_SkqLAs12VVW9z_RwdXYSaSIu|Z<@|DJF)bnF zwlWGh*x12%q9jCunU(umnEQ18``byVJvCG=p__1GM*NQaT&8XM<*4G@Riz@Rc4uOw zxs{wr!0gZji68~(B@0tzoHus~BIV(}?-kd$ zc80o%VVAFuy?1eT$|J8oT2=0~8A3(1TtMK<4ql)C(Deh%K{V%C+n=nMH+U`FfLP%v zt~&Yf9Pwxsidil}T-cY^Z9OT&`R=EM+c7F%AN+>p5t?xP!|(IHoS=r-o*4arlbOGt zxk>_UNcaAH?}1;9C<6nMqujEx)py-0ayZ^{0tRx(!%Kj)!v%AXpxRC|BA*@{7-8{F zOGdc{4X>KTEyht8jUqQZpaS6NNSp4&7qj@$I4^79gZ=5t&FdxzhwQ(*fBUt>N6J|B$TNQ91FPcd)rnEL(DvlO zh%pYoMUO^T+nZ5l=vz9~7AzumVy5NWeC?Y$+p!X{3|c(M%n$FAo`tmM-#p7pll&}K zneP_QXFAtmw{yQmy=R=^%c^{D7q@l0g{@Ge@K(&vlp%cUNJyBd$EfhD_>2YiEAKW* z*-L9;n1sRbtL+UAJ~CqUb8*!<>gV~L6^V2|T$P`&icvY+v&m3FGqH5h?Vv;9H$*aoZK4dN$G0tD zB(O6vlnt}rSAX%Sl@Kuwf@iM}admtt_RyGY!M@1GYh%TF>#DyP<({A4?=a1doK=Y& zMzvi;;42Tg6#@#CNFb?y@$&2#ur4t#DF(IsE4l@IJWtHJHDr3ght4$Mf~&Pr2aR;k zUL#6H#YC&5e8yr9uJL6;sLPU@3~;GG`b2F9r~m|*AZPu*!~s;Ieea>&zGuCrws8zJ z7uo_3n*UPufeo)li7+Q>BJ;qres2>fHjTRxBlvB5?b~T?k-(J(5YunPTs&a9PR0=E zc?7ik-$H7%29iNgrDGb#!H5hxIKTY6W@VJ1GYYuHkbfP9&>4cPjS|aYpcY; z)p^Y!=~m3~0}n!&xMId}4zHN(fCMHpN->%5&U|B|9@F9Gd#SV%xX^OkK^L{Pg~O#; zI9bkA9Yf%A4n8M=05-b-W5AHY7x(wQKWcl!G3^rNSbj#N@1eh0_E@2+I_HO|6JD&g zzyGg;Gd;7Ks|Z?V2_H0{dePF=OiYCZN*zE1H082C#)1ggpCi%!1*53(CH>Wd8`+k! zBlyZqHDX7>*~H#9-*4}`@pdbpYUxZ*21#8lc9`-l;^AuyFz8pB+t4zO+Wb2f5axXT zT5ck)&`i9NM2QIdrlh?%mceJtnquMmcFW$c&Gg|EXG(W)#0{bwzJ{#}iqSo2%F@jJ zd-ybEnkJ^ST95q8nBkB(20x4eAG$b`lJ)5%n!OJT**dh;!@t!|Mm?& zCyatf-6AI$kv<2Pmjb1HoXoztzkQyt*nS8&oqvfKOHMLe@Pz>XYJ1ItzC1`uPtYRg zjXGlN2RA63E3>o6$*U&eio7)16wm-2IBe4oC0w`SG#1wv4#65pL$Zm!A3|_Jd&chv zTqCkE$A;CUe7Ll(hdzjOLnU2Z)oZ2Ai9ovW0|cXdR^Z%Xjb)XEbEZd!*-1Bs3r~Y2T6{n8J!icBpfoNR$q*9#z{P1!-Ufzz++pv z*;v+0%8GTeX8a- z<>S4Mx}a@k41<&F`L?J7L8z2+m6-qawI74*9R>BIGUXpv}l!oc)nAah*IL6<#k$V^z z&eR8?5+Mtq9<&Qkzo9!PfbKf#(tXD04_)P7hVgBhzt7`kY=<43{hT+0{{w;j z#@n+ADfGC>_hSw;?Xnzg*jt>^S{$MEqdO?w_@lE#IS86_y8Z|3KI8>+b)Jo*dFJ%T zzj-?zCzxwNnYqsx`p~$Ff#v=Mgl5Vf`5$3|2g#s;Iy$kWe+WssR=T?iz!^Y- zWwBFx_xfwg7gx@ycMGJY%l)R}mvx8bxitH=WWV%RW~{2=mX^t~?5a^wkJydO_~W7w zI~i}izsEamr}h^0iD7`~UA;0!HGaX$ex{*iS$VoD?Rh6E>zYq5G2DfggM;9ItT+N2 z;{}6ST3U9gN}KBVrz^-u`YX!CK39~}pYCkfIom%5Iw_ienfJGm8IEADMJhhk;rTPZ zlY0ILr@AMNe0--GyEI9#c!bt9MZv^Kh~1QfFYkjWP4sI;g}HLg=+_GJT(MhB|Fny4 z7#vI;X%}SCQr8=8&96T0Mm>3^a`oq)$KXGQs{{SU1<6bhSWENq?W9~ou~DsgI{rkI z-lj)%6fVN?VN*Qjy*gFvTB_rpuIf44JA!+8Q7P-i_F&D3$RL_m982qh1o9QBu*dOB zaVRtPvI^8}N{ZR4LxQ^e^NKMcn&tMkP}_ z4w7l9aEPB<%)4Os?}-Xw?J*?1H9eNhC=RL6IzA{-+RI}waARZ$NA9s*+E!YcHgj`0 zpF`!0dOaFY9}nT*yQqe9=C4}E#*!V}j8R0w@VNP4-JtH~_zk_wpUpmAyB*ciwl5hX zto2}2Y___G>&cvq-b)4=zITK(W0k?l%&BTm+eXp(x34PF#cwwIQoBDKsF~gRc z1Lp5x&ub(Om1wd|Ps4ZSF|(Pn49A^1g@U{pk@kSK*&XSvS@fB|Pd$B2Bl>E6d&F1G z{n5ZvKXA2*bjHTH)Tq7Ehd2ymv+%RRX<20_1hN;jk02zE;(qhda|?HbC}P zUWMNfGMvcj=xymcHMheUvO7a&F&-;e!{^=jsi4cJZVfeYJVvqU`qL&}+>xe6&o6&9~-($OzS?rc5eBn>8 zr@CbRV9WZM&rf~k<8W)Ms>%&M57ptORH?3>M$q8Nk#0G2Z>jbMFy~*gC1frYYYL_8 z0V*>7?Jd}nU)uw#qc2xfm7O0MI?;Z{MX!oM%XFLBsH_&?hSO>rRMt*lNl_4Ijto1o()j`8(zEjLf&U#&80Z?=eEC9yP2_XZsHXt7n0C0 z7B#}U@Mhj_C37EEf@6+pS1K1y5VO{m-1FWJpKK5EhZY4O?dWoCo>@Ij_EZU7n~MVr zI#R*(%pUfe!Qf}g4qVf8gWuG>Q5$Yj{>4=`NPv^O#FaC4PHhuy?$dqO-HB9gO6b->QKp1) z+NV3EC{hlgEJ=zSN^B0>?otSGmn4bZB{^F{&RY>eEHuk$a-8$b3}c($>;2w_KHuN{ z*Zs)*@OoXZ>+rmu*LA&Tr#z!MtyI*s1`!@M{l0uow<0}m_S=8wKGtdkr8CsTb&N`q zj;>#0>neIN!UDl5i@sw&bn?v0-8F8f>&8yiFeUot9z;_+uS-&PET)N!{-ZtFeRI|& z)~IU*I^Ss8{>Zj{_8s3;u@CZJ?JL$EPXrLplU8`9v>jpJwiem82bY^_#>vZn<$Nia za$S@#&g|89Hll^Dq1N&}lZeIqp)=Qbox8_lK~VEdxKPP&+|Ctz9@_ZkdV*z%Uh+pz z+Xb@$c=nN_>h8BlwSyPrg0!{#k)8YOTfWv}&7=DDsV`W`EU;F_VBk;^jx}@~gHOg_ zO^6b?hq?j=eRC2S+OnpfMVNOx01y#av+_PtW$D-E&f|}-RK43&>~DcX@9328M7Z@w z`J*V_m;e>A8aS@dXc*f6@W#4I;)m&j7Zl;Qx2;2X3?z|0^Rz=gGHRhmM{O%=L)f?c z4z^DJ-srk~^!^%5KZ|uCxGGtUE=ZT_BZ|6kr7}fLGKYyCtLN7Py$P}_+*GWc&6F4| z-X8be7wfM)e@p$#IrxL}{aZh6na=R^8@pbxf31f*hu*mk;Wpr2Dr3u!yo-1S0qxAK zhG@U8zK7lX`0j`MN*k@Q6V3^iNan|J7ZFtOtk_4;HGXp<`fVG{75J9#Ye3au_T%$s zSpV}luigIVWGGjKp5M7{*5YiBfi&s7g7j=z%Cx{8gFQ!_JT??RRABgvFt6zGGUHUf z#h`Ud^5qjY+nSlphxhh#*9S#GG%aA2x2eU7JxKEqIFGz_S@>pq$7`kYmkUft>q7O<-t$)GWVvT_qtnrp}&KbDY%_-%VZD zp?1Z7g)Rh2$2Z#`IAMxzQ#evOBBJ~_tDItC06pfLI7ZM&tinW&zH5oyMN&)!pdwV%!Gbd`X z;`l}=A37v8?2atSwk*ln7c;kor(f00k{J-Wu#(hJ1%3y@P(^Ny!J{?nXe+t~U#>ms zeP6rnkNdPoFjrdWF!FCMLb8S^;pCEzkGS8pW|22Rc%|Uo(%4m7!euP8@B9G^v8*n8 ztwpy%pq7t9e>h4JvO3(cC}R0~ds(3Gs}K?1o7(lKyIBmFCjwQ5fZm~zYZ*h?n1)w( zwI99hXi5(DD?~JYx7>E}@yO*?v{9eT+&k@yTrY$IUP`PIMh5Tya8JvrnwUD$YfQ#C z>z3rDT3?-hl{1eKV(OCFYTA&mhpHY`n=|<$ylRo*dvsX0=+ph6;iIMdzaIIAd-w5& z4j~badqD))2_aEKlyq{jPartVR6`~wN|6Hq!>D-(iInj3YWK%$>mk1#;oXd4c}9PT zcLG}cHosc&8o)ZZqI2zAYtP8*TlcUQLa2dXJg)|dZ7nHSw* z@D9AMI4TnhFUtt&4EwTFIt;UIFbZ9@UZ`+=r6r?YhmOuwh@6Wm#Vz zQO`W5`+565`?kIGj>h!mkNlj^|5hIlY1yzIch-W8@vuX6e7&ha+%x`M?0%jX<9u#! znrt!0xNK7a1>A2HPfv2|USdgzxxOL7yaVO|tfdekQS#-;7FXqqm*uZRUWV>dptJtr zJPEwM_t+cbf@TD+BoPtm{k4TAgTaaj49{M*1bO*ivF@9)-fcHF-;`IRQb#)cLeTeL z$yzE|?H_3Eg9id_97nHo6cQ^;gPq%F~PeCR6NzMUPvccZ@5~9)jS=yRI z@a00zXU)p}=Nd2`pw8%fF~BS=MynjWlH*&TLGJoXHG9_|YLKSFu?R2?P2AQ8RZr6S z-Q4WSz=7@|1ITP1=u#SGNMZ2|$D+ zhC}N2tSc+pbl=%vFq}W3s}CF*YUt$u1qyy?zYTq6WtOONj_$K>|7wCYe>Al0PS*le zTzw$yJ5X)Hv>SeimA8sa68j^X0bCW#>CD;Cxn`D^GEL92A9~R2%DAi^AP}PZB;!&Y z$lcqwzugvLA2&V>*O?&)IO;utCO$90ZDcl2_v_KaNPNT{XS zMES}GdwLy3!+yuy&8br{nqZI{c&m?SGs6)heS=w0!=(0q+cj~26T)p1b8NtLex^nr zl2CJMjDJNWx-4w_XmE7t)h=4QI5-Zp)kjoP?%2He!JaDmL>AN08KZN^k~b zv*gfFX}+p%=|QkWcewuov=TWIjqr0cB~C(W2z9TLOqbJzdokL#)2&f*G~+Jv;|6F@LzULtFLl_=ipn_N73;t|_GAOnVMUNW9Mu6q*?UL9PS$mm8_&iPRS%T19W^G=X|aKf9@RI=f&gQQSjD9lbZU}iwXZ53Qn)<<~= zi$Of~qsVx9{m7B)tg`b&2?{3;)}-raU-M=^7+|k8HRq^8%RZT95O}hE-^1xtvzlAp zU*}j^_VOW>c`ne^7mkJAA1)uKn-=IDBGL|FO2yu`4wPI$8tGO-oX+NN+XNuDJ6Z5} zhO|d^N?gB>GQ5FSZo=dL4eo0}^^C`hO9uwC+s<~rtZ%ZIv@p=qSE08xbO>&oE+Tc` zai9n{g!sohnda|dL{LZ=)s*PmEXMnZ_{N;XcQ!!Cw-c4?LM0J03d|-*8K83XYvN32B+TpOaWyTcdpQ`{=NVoX+o5bo%Nb8eGkXz+rk%`K{TimA4oJ z7DViL(UKtS+ck2nVpkQtr})R*ON_Th2`ROF+vQg(>^x^#drDw0 zjN==w#=T;tU&3^V6|Vci=^8VIrzm;e4_74|RveQerMnMg*X0zMW%9Od^6&_j=`J0( z;k`Mi_#^yWJT+s3yZa`h$KbXXGtWa%UW-Tet)*f1^|g|?i)!UP!c9V@|C{~FPF$vB zO`B0BF&x&y1jqLo%iD6VGuE*utl;mDNVrQ?| zr714PNDudV%(tGhNLSsof0g!Cq=7a)uZE{77o=Mu3SJv&H&!-H^sIcRohnG*N3uFz zNsJP6PI;1AaTZdmY)eSe_FGM)tiy&i9c9OM2?HuRBk)W~UM3}?pXSBNd-rk1dqR|V zlcv}}6FXGxTb>+Av5(Ais{>tLkA)A_oqDgm2T9Kfyw#=Y{Zwg&BUXIU6ewSl)Ya(i zPf7KtNIP0MYer4zBS?{q3%WfX;b;#vW5)pRb#(GDf+`Z9VKWKI(qn?FTlJ}(z` z2wa_@1y__cC5}(|eBb*e!|%**_AA9mDecu19%twAuU6-GQ6&c|!d$5<%$=ajfgFH! z9PC5!{FV3Fp=dD$QxfvvUM2Ox2XRXf?VrFPhEauGTeBolQQ?2lk#AQ9!!(ua@tm|la*62gMTZM)=S-|Mg=xr#8H+Wo%8|nzW z#=U{(%g;Mz%0yB=3a`-Z<-|I$W}k9$ZI@^5EjcR^=miaNr49-Sjot>igGSS%UEjKF z;$JMBAKIwrx!PI=r1TEE$YJfY?oqeWR_oZzm6UT&*BUIee1NJ8>>Twdd&!62A`R5- zycHn>9NZgMENrn$zz3z5kN3ZqD4)>DuEWu{30{fcf;t2m4|7VA{P0q1oy$b_71fS8 zYdZhdc{;-4+Z6}5LpUkHUGWXXfM=-qgGBJFf+FoW?1|XqapS&oW$o}M&hSCOUyD~d zqMtwusSYzM){VuLB%jlLZ+Z$6RH4&(Zu$Xe)mNj7nubeDwI%qmNIbWnOi{Zgqx7R& zDCIZCJY)qY7i5Zi=N=$^!Jc@(%Uy)LU7LpkyQ%xmrP>$DOld#efVUMtP6q$ZowI^| ze^`34m4sNL|I8Fwl)N$3p4?-7kQU8K7DG(Z)WeR3DcZ5cb^&6E`=-)-AE!;ZzVtVR zS>edcMK9|{ybqo2$t;~<+mgKPx~2DBjiI%VoLP}wsj9fpux8)Cts?S$pK)io^m6F> z1pf(8RTOLa{BD$>?2^R*v8LehfR$6{>kRxB494}f&T9SuEuFaaw>SRVgGk`lqm zJH3t?-N*C3D3f)F6)9%?074#1$-{2=MWNGIBmrb2=o+7p^w-*xPVJjHA8-*hb3>bV zQk)ookKy_ldHY`cW3g#8ar$*>M;vkPo3E!Nf7sJxmwLJ z>{x-9c93LF4ABZ_mtP*AQ4_VR2u{(*PQOv?I2T47PDe4O#El3QhGjf&HNoq9hZlPT z1*h!u{VRss{^(Ah1JB_vWcXA;p`abs$sIHeHxC{DK!R!AF>Pp7T+HVFjMe47q82wo zxOWby&t)fQH7ERLF9xidif!_l5{X;ME+0kByWkg)mB&~1^`g<+-nTH=Vu5kxbFn3>ZPI^1{VHZ_xy(|uzNX>))N+fn^AgAxHBSWBq| z;N*7}^JnwO_5^oS`q)B0x^z09y>EXi(#Y=8q3v4iMrfRM{QkS@$)34ear$S$?Tywe zvS)`37n`9`yTdktU(JiYqhOc5(0OW|F-0LurxQ(K{5awg`h=kOmM96DV!=vIX6cm= zq2!_edwl92rLO)rblJf)TA4LeRDzUL)oL!;P6?Dqe;39VIJd({Ul2HT8sKFwYD%0% zDW=?}ip?-qvatlv2JIr3iVk4UI*E73R%a%eyVTe7yuj67B%d` zSrhSDEkZI)zwyxKTH5Lkwh*-iOo5I(aA9Va#rRB?NbhmSKoZg@qjh^Q@H(SKUu3_| z%oh3k8qUCt!wt40cWV&}t15Vd5P6Oj_EW_V)4R`$um*jk*L%ktauT+3PpzGh_0bIL7ZXS&WFCF2 zvfaCu7%IFtag+IoxEEj~$TF1n-YLgA)1hiO$QzOu6!tXJ`AwO|A=M*C9YEZVKzgKWa(y ziFfT2AuHY&iVtW}Pf}VJSjG4u`bTHM+ZQ}Rf7qrJz|3d&5yhj|bm~N3$<2=|*uao4O?QUsk0Nra%rpd&+s7 zU;{GurFFPkuIP;Ud8&m`Z!+e+c#B8~9IdbBsxhj6TLZYuKFBAnB zT9mi*zm!M@ZF6%${j*HG|6&#f8lo0WyIo2RiM@o9KS(3@lv6c>MYyY~$C*Tz*Nkd0 zR$l8Y@`Lt{5{Mf$NjUcEg1+#t#$xLufOcl{jj8VEWY#Pl8R^`Ns+mNiuaNa{@-T{3 z?!AGwF!Zjk`?PpIc~dGCkvMKni#6wf7eWxb6E>!ywlxg?VM|5-tVd0(jaI=g=? z1!lodCYZEpuWUk&TGF<ucHKQ{E4~pq31`_8N9SgTwtPM!>_(;pq@XkmEf_>q zFEElL*35eDcq8C+X|7$=ypV&3lFa6nyIBe_>1TyvMx2FDJKq2FjoGuH2+?dtIEeelJ@ajnFy6JREiE@*f3e-T0<`zYPC*!~ zOY1872}G!-7&@UNYLM+wPcEp;nM;euVSExv z#_n&bZ+g`m@ChMRLT$FwFBV$G&b?6+J#&5WN8M}?%3ww8nu-n~469yq+7f&H+{*h8 zQc`RjOL?yj^9jF~D?p*Sl1&|Savli_WLx(z{I zB0X`gECO{Gt=HMdaSbo}&F(mOKuOi2_^y=gMYiM+CAS{@3n)|8uA@iyX2$oha6rV;(GKb7J zckoucuV+ulOvke@6i7S)>5@thsm8MP4~4Pmg|g&dtO+ach|jjo4U}3`JG!z*ANF9G zz?vUQdsxT(WTS-Ae8hAz9`~=C>HtvuE5RBT#9t5CC1)&|LCKl&bKK;!(xh02Q48Me z@LdCnqjs2^!lT^nvQsl}Fc>}Bb&#HATw;G1Y0$}BQ9cYO$5vUC2n7?H1DJqmu`5IT zg>fomz#(uK#*osvMj|x|rFq+!w_Zmwdmr0s2o1dn@MS$Hkrw6xP z`#LdZ&i1o)!kzXyKHuB9ED~h|d08URA-StM)4gR?262zTU?hbbY2w47PBdz z>1zNIMPRG1b8nE>1kvD9XXe&VS8I12U~uL9Su#u;{ZU9R z7d)g282cXiSvY=NI;GR}7F&qIO>K+t%&21I@YQ7P(a;&>-vTh>$l|=(Ot6hd_?6`3 z1G<4%A}P*u7u8F~gqG_k(&QSvd~}rL*uX(tND;Is*LVj3FKaPp>#FL-?RIy!qV0eG zc3y)_I$VtuSOY)+lcF(tB43ng6HORvp{-2TNuHcRtt!6=H%Y5jbu?5KZ5PH;VQt7@ z`>>bXfwfeX;&@N3>kUhuwcyt>Q7wRV8OVzL(m4&)!LsaDWVdWAUYr}8xt$S*#a_)g zfI`nZ;CRG)U20JL$2@2e4)D5Mbe-*zZ;3muwO?BQ0r;KewdiQr+P0#NhVzauj^+{W zhsC^4*En%i#Ww!5OOFw9b@@ez#qm_l(FN6HP_5n)gB5tra*s-Xvz;MFkSj3gZ!8cATgad9YLJanK+rt9p0A| zex#ieC&zd1(xJP6tP8jzXOv=y6-~!ni|GG+m*C(>Ck9{%xb_LhbbZ|rG;az}g{SK;@>S1{tk`d$Xz#88zB#MJT>geME>^sC6pHwjAQr=_~KG5N8RO z?{?{ddUw1RCS2JI9?VYRq>qmTadHc_kjRQ(%YC7Ary!c!F7(?vwrQK>mtziNY6V zM!nxm{-BykbC^cv_r<}U--c+)@ zL+B^~aD$cF-1kX`z6z4$3bs6K#1fTWG&WwbQd!g@(?i^%rkIW+rRW_%Sb$r*2pgxJ z*o*dA8}&`1+{0pHuVHDc%R8W?(^D0kkWl>(q~g8~Z`Uw)%V;k5WI%@8UL#V#Xs6R| zvmf1JpX5+yhxRr#su(B;n;QUF+TdZDj(7ydDi)@E37CmBRJrY`S)p5!@p6j$1wz+p2iLGExrflc-bi?N%SS z6y|qP<1X|3r=Ju(c5v7S7Hbbsy(Tgsmp45(#lLg1^p>LLS|YMa6(Y54p5_x=h_%V6>MVx$9CoRhVw9TF zV~H#EAdDWf=Pr5BmUi3!8PmguGS_ECWGGIXWoEyUEO&**N4u;<$<*$&q>(uOV6uOv z|7qpXBVDWnC^MglU3K}P^pQoI)r||lx}=nseK8!8#=4Fi0_WBZkw*w$u~D_wl5}^^ z-^TkMQxt>ptvl3ytAw5s0qs#V6KBRRo}4(d!ol|#>a{(yss}5~Fa-BH8u}qeWC=N< z1p#n$KdozY+_Lb*R_kAvPKh#oCJ^hcoS$j8{Lq7k`&2zRF7u;S{4Z@M9#v|95i)b= zanNTp5Ut3M=-uRwe6y>BBQCa}1UCmWl&ZnAEzMgf6=CUtm;h@rErHhKcGa9%ab!o< zn5$!wVo)hm04j(!`z|aaJl-*!%anLSXeX@EptcN=ez54U*p4Z-HcN=cvVa2~K~Hcd zw&7N!8han}9yz#4fe8=3q9$GDIUrHnD*scWmfWj6Cowe&GHk4C)>!#lf1zgmGt|<& z17v9;2662iXnH*GKj$nYlGs2i!SH6?Ox(M?Zc|c zdjFVvkcvxHTLR7Kz*L(eHzI zIrWjCVNs%fa)Nc>f*c}aviz;}&-+Qvn&>I^nSN_KC?maJ)(7gA@3Kl5jR^fhYcil#v${X{!9j@5}zFl=ww~4D)v+>H^HHuA(%) zQ4H`fN(z==0eFD(3ZnH@GQi-f7!4BJdRJ0i6Q6>e`@r8uv9g*&+L<8+MA_^5^zqu}JE`9NdpcX5Pjwwws zqh>XoomuA%ofU%Qg$N7O2Jb)Fxr8$2wVonH_`mD$$e68tae91)3TJSOzDZf77=4I8 zlU0w~^A2OYIuaGDy`qUOO7 zh2&%U*IV-E`jV$JvzNae->pDC3z@Xw{C&UsX0}#yV*V9OPLunih%v#2;cX@de@GV21o2S8~=Xwq5Lnh_V9-z z>|#ZHKngo`qG@hw6n8~gSJJaz1ZtYkQ`4ayn}uoi84iKxiM_!I?d$&Tk`_Xc!0+9wL3vqRZ`D1>Eoz7|C;B3P zDNh$>ri+@)J~7wdHJPI?xhNtrDY(N|&G&lB5wq{>KJ+xfJv*89qsYz;4I;EoA%f3I?U!_S;^58WoDp*1$}dZQ*((Vx9txbaGJ_6 zl#HC~>oB)Q*J1(=ER9FWvl}Ioqa;bR_r##CgF-#192?*@IIdVMG|gn45^TFSO4>Ja z=A$Rvg_3_joO!9GjZ7WkR{5_v-mkkHU<6_CGXM-Id+`ez?G@Fuj+dOjyp4@t(>JHc zMv~#)WfB$qPQ=ip`{qdUnWc*&W<*U;Bu9pWt4RL|Y7*LO&sd6;s_#!9!HJKD;s&?$ z&V!H#n&6rLg`Pp8dilVl>h7V-AlY~GJ!CV*s#z;~BQUa!Rugd56VTe#o=%65`c#t1 z-&Lp=5Ol(~r}G(hmv`BArvCip6Ttdn&zd4<=*H=p2hpo8&2$Svwu4}6M*t~kVS3y% zUzse^+xX*Nl!bc09icc2bX(8s!QIH9CsfysnZhfVPp!Ds#gc)0{PJfDbXA0Sv$rD2 zed@{?3+-g9(Y2pnux8c>vw+gpd4H1~q^Q@VbN9P0KyMcIg6{F<#6Lk9yhSK{v(kIf zdRG_f>H`4wA>-OYGfTPWG;rBltaO)OWy>#@CBXxZ`M27^cSO$ppYMne3!(;@|9MsMuNYnhto&7=*kqAAnk5}n43EHo7I6rP ztLbF(uXDTH9#~)HI7gL@R|stokRw%AOz_>lF<~`HFyBd!cdK@@A1q4vWlyb>Keg3c zb?yc#av-O)D9sjYUDt{}|K6$g=@q;8dmmS8#ePv6XDzQy;U7=5qlgOWB_Of^vun8wIIAenI zh6F;RbL?xb;G*v=$^YaWPE!jR%i?v+>P3z7Qi%Cz=4=N!y^8I{@!i$M-6Oi~>f<4g z_XMJx;-URNxWyCDpUjyp<|S9^HC9!gy9M5r@R)sSLplQWw5fKjnOj$j7yN!dtkd4O zIUl~RuO89^dU-xqkNj^PQ7~(CGNH9BhoKjBNv{US+)ZJ`-eV+B3f~!evZmUJ9T$^* zxX_ktzR%pUf$y)CKt2jRpp(t1moMJ8l7c9MZgDf9Ag;30$$p!VxDx%HDVOIgEawfw z7|hSmU#Cl)jaZKlQ+CLjRr~Jxh%v6$IB96eW}y38=B{<-9!FpPn{|;rr8o3a`6K2@ zTT{bWgnVUfH(1mS_I~^3e(QM}WBAZEVLiR&PKKU-m-`w_Nlwsbyb+_aVSeFSzvBq`QY8?*{OAu zPCB&4l_Zt&3?Ot_4~li zpH6zR13oK%+5nxSbCw*-B8y=><`=mF;LW=6%ux6MbjD3mtwG2dp_0||F6%$P?O4F}cvUog5>hyj#@2oa?wz>zyOEH{GcpJ*evE0YzxdlM#{{;s&JZHl$ohcQk@#vZCcN1HEb!hcP#l;bi#e^I1UlnX>gLNtN&qU z>V@-$aHB%JPODG&#r3{FABne_N53Ke(=Quw^S6=%(d0+qz~Zk5OX~-8ZC_ zluV5E3tUq~|2RZMBI0@)>4QhA^za(+!Lp!(jclA7{o*?IG=||kBGMZfGPiHkm(3ZD zk1@PKc-zgD@=_NZxN^LqkuVoAl#ek7K_f7MA-0ls3i;d+MIhRa?-AK2QkUy?#a;|0Ek z{_-RA-1oFkQ(klojxG{Dy&rWjh8&PRkJ(e1{{wa~-pnnf{-yogoioB^`NqxLT;$KN z`X~P#ir;Z5Km$7I#P;}z_vpb~NJyM=Z#JW6f``3(B)8cwbj;KU78B9)6 ztq%97gOunV^0rQqfL5Z1bQPT_z7s4&k&~1m=-d7L9amc2yvQ1j=;fx*jiMF(fWdG| zn;NEE3P?y9J@PN2n>^UtUm0GkFhdTLV?tc`QSJQ}cNoMmNaBg~oJ3d_4~q1VZi3r- z6Lng8_>1rn&G>~;$MK)oYsP_4ed8!>Eo9n^^>9-IKjKaoHyzq8?}JFv_0)7!X$af0 zMbC|D8&0J3P?!!cg8Y+(57#+jjhxGvB)l)1c>`;B%U}v53GXa5BW4^k;;7-3u~eFe zX`AY{N|PR;hkVbK6AU^JGcuYlw&JPH8hUt5cvZk7FqEaF{H=#LQxm)qv0-9?QUuh* zyqm2Ed*?ERGucx~>k}SNM(2yCN7%d#kd*=xE~#S%SXNLJtV^TrrJ>6{u&t+sI^*w? z8}Os4(s#*Oqe+lZMp))jvA#vlMrZ?!YyQ%|d+;a<`O2#rbS*#3v_c!JDo~l{u?IS1 z+b(R@u4A*O#FE4Jm|PHaAIo!j0C=DbgDzJfddWp z+aNWZZtw3?)c=Fa6=_TZlkR$XDbSKj;iWUvmUi4`r=kCXgQNx6iU!@ zp=?YWU3GLYUTd7E412k6)_sJcPLZ{1cyqg6bGxpLoH=56MItrdx-_i}V*_o>&7e{X z{{%l}6Y#@gSx-6?!xcQIM+Rn%P20MHOY;PeBXqY5M9@&^9uj4 z5_5gwIUg)7kk}ITuiGkVUGR~b8nMd6Q2>s+)tUvo@hq^ICuc1!*OrMl))^o1RI)v) zhwHnrf8-E;!e$OwfZoeaz4@E4MsW;mtRZKia}%YNj#4-lSH}!&Swq3ML%{)k4vqd{ ze1lo(P$DaI*^Y6@n#kc9^HKw|nGdU$Q}8fWK$ikE@IR~K6mWZPvb*{H)#-wB=VkFP zppEaQnJa0bf2iAU1&8Yx7nxl*|I2*u&G~B8aAG}V^e_ydGcQzPR*6M0Uw*;Vy3WJ? zxU?%-u*M-k8d}Nrc75Finy6^Sl$y_V9ls95M_zk}mSFnqwV z^uVr6;v+w@HKvoU2$2oAe3oiW?B+YoJr0>-Hl9t@>F2&gA(LY+L2iUIrdiaJg24$E zR099a8tGeNU^zf{%)P*Q#t4S~f*eYpFhfSZ!zJnz*t2=#ldF@p`q^r zdV`j9<(dUAquA3=Vb-vt>Vae4S~Rx%Rhqi}9}7pb15YxLcbm_XtF(rYn_gu{PGW=o z;D37VLob4GDXhwm<2(-+-oVY>q5uaw!qMxT-LSJ7lFl%lCS6&(__97lWN3=9A@km! zX$oxKS4hK*7Br_VAlyx$jN$Z!9r92zb`BzTabLoiX-=n^kS!-AT^Y9UBtkd9N34=Rfo)jnE2 zbLS8Mz`}(W%<^smoZ+@RsY-8tnJ|Q;@p__mH}He{K46YqFn%gh)O6Mm4T1@A?8cBO zc0|!QF9Koy7z#(Eh&s)p(&fRdTMN&X<`NY=%;bzM5|h{Kbqdiy=ZlErOh0-3a<<7r zaGG}{>FHhPAaxT|1#o%hH|jV0n98lFW4C}b;|{R~srO(Y z%g;^SUIOy93*fSF9U)T<&%QjpC&SzLR|@_UbW+->i9dO!c@-WvF)hpFLD%7d{ZYe4t ziGkx8z<5$pQjY>_oN{KV4YPHr&9;iSXi`$yg~QD^$xEXc3jFwh#gGfO>H!GYNuE(`F;IpVohcIM;m)uqB=$n!vZ;-)-5P7EH}e|OFx0UpDT&yy)}J0wPe5+*H+ak6MZwS9Q|YFr&@xWIb?`;R1wMs+5Cs8;4CX^a1(kk21qqjhW_(n0hn zT!J|VfW4D>tcH9 z!6uHRE9%#HzI&P1oIgkxE5$!*2-7$w-C*)#P{`-ZMlpO1ZX_U8%v1+t0_!^VbU{@3zs%QH$DnH#jeusqqZD1`%~!_ds2*SmvMTE(5+|G6=03J zl$@vp{#zLfATXQH5?=dl80=EYJkR5d|Cc4WX}*;dU?piP*S$P>J?}1jd_UqNLPJ7A zwqB_j1U{)YIN>kD-u_hD^mie+cHlTizE56f>x%t*^tJ_Lh-Y$U``G8B;R}N{Cqz`P z9Q(lHl`MoRavIUV|4CQCQP)h5W=n3T*{e@ef8xVq%AsfsvU*`2H})jX0~M3ch)0-P z$}(XFq({ftkXqasw8UA}&9Lc%*DM`MDA3K}VgNk*uOC0%Xyd6@dCl4bMbn%g5=s;b z9FqXY(8iPFD(b5w8A^jihg^)F=j?b@T$%R~b6AX#F6jX)>>Vq3VXsBTw(v(+m^rhA zNheLJ)c=Mo;-T1=qX;xYMzMjjtbz7t1WW-`1)z0QV0p)!wIb_SJ%XS!qN z7^K*|*4*So;1zz_#>0XvV|hh`jXm5K&=rT|r6H~OkwNm4N=^?6+1-$!cc}!%*JV&Z z>~JX;E*8Rl^OYB@&5u2xS1+%$gK^4;@EHDX3YaS@(GMHI2fPIabevY2ZbK~(U1S*C z1gEjS7mitGQnuzc&;Hvwm;#RZ^Ri@aZSlm&#h`N%nRz7l{N%(q=abs#4wVGdJxIOH z1|e5;4p2<(nfTt@7OvP7(YgP9&i;iK-ftsoJb5dSY)4iiK6zb_(*i<}o0y%BIR0QD z{_v%(s|2^hkrhJEEpZ6SKu6QZ(x<*G>oI`*9~N*1=|=!B^Z_zA_{`#FtGo>@mHd(& zWTzo8%JgF1&Mo!6yF?ita^KnThkQQ;*MGtoFo4hFB*^cRvb@iFw-DSO7G`NBuahd* z*q=Z-w2G2{0i6DoeRJ}0s`3;HP4u$PeBhh(XQ*Voutt)$8(Cg<8hQZJOFpT%_`N-C z%G9oc&4$1{oTbfc3Cu4l`7tEL3q$L);^SI95(wZqF!__u)#juVBf2vNOH&A;jHuIP z?6z;it~TLxqu!;?%nPNDvoLN+(0eklpNtVlbc(W6hj-?>%33&5EW`(NU7aHR2`%}- zin2n{@>q8O(4Y{+%Gr5Wkvhx{p6XrJBM)iD97u{GQ~vDmhTB!3r|S!a%B*<5u%AS< zuVBd3$Pqj7wo{Mbwjsd-$ZBB79x({8^j>?8W8BHY34e40_zf4MDXIeho^2I^h;hZ zTU9kIf zlqY<1oh5)?9_j!%9#31>vva}2+KtcNKKSZJ{ij)mBkwWn^*&km;SH{9d&~bHzZ7Wg z#Hn?b>gdc}SILh`Sqo6=4?kxR;i$z;l}lZz7Aebk()7>P#HM}hS|~H+-eHPU%kmK_ zBH{R)f;VE+6$1&V(*&q9p1Ho~*y5^K_0>n`G&%))ekGrRl>v}Aie^)t$(KvEirpj( zK5t2u%vR*=mea(nBVU29d`-s@SBJ|~i4mp8$E1i+4O_REJ6$ob0WSy18mtQ+uX6g> z1veD@R!cn^PSTa{V1_beeu!;+9FEr0-6nHRwdSpn9Ty46aT9ni`<)#J-o)B!d<{~WQm$b{eoPC z1lP*8(ZPY)O|LrltMd*(*OQ$6k`s4co-8jHK|CY?HbO6Xi$>x$#Z3&|9`RRF@@>+U z&%FFU;N=`rPu`8Js~7(xrXP~{QB?FiYsW0QRm-~QG<#{tO!Xik)r^!uls}W2PkYyp zE+?`%p>LeyYpl%7tM3G`0Aem&vCldJ=HHiK6hYXJ)-mRO@P5dMfe#fh%Wi@??Dgj% zGha9Fp*Y9gW7uWY;PD$8F49^r{U5z*=MA0)=&FrjVq(!DGuj!CdIMt|uFfp&+7c`Z z^lOhfr(Q8Ftx5zNz5tdQ3WK-aUKh+1l^C?)nH>oyMr3iZko3tt!=p|aE|I3%U)WxY zp45kw@%;RJZoUCNr0&75-m z-eS+Dcf&bLh%gM_8oy4$*_hV1^#na)@5dFw&Zv8Mcd4_=Y^3%05}Jy9A8~pkbym5g ziz`dMB(yhCfHHvG&d4p#)pwv^rqJbg8s_k3FBKmINs=s_SopwE|JJw-zkhcgY(`Gat943sXrRjTpKOoxRY#MJ`*>!VA^p>9cko7O-lXG2EVnRjQ zAm1{tdG?ngIPlh zEeOQv`Uyi_5&CKoIvKbpnDl@^jL5-tnLOFhVSfGoO(=M>{#giCEF1QmwbUrZw;cUL zkKnl>A#$vxA!O=l>j@D@y~NxZf_J%$wH-Nk_g_n(Xsx0I3xiLfnXdqFWiv06*Gm=p z48`lZEy0hSPioUDRBEPp%X&_XCp+?Piqz=)bph{9Nr2+cW<{wDcUaG%Re?xpt1`@V zDB1h7^QW~N?vb)&4(<(4{}r!ON$3WMIAXd=)@+IQuggc&7PIpjY=_GS{ru6HAPp21 z8E&vW%G0L;RqRj>?GW3cS7yoeF!PIR^`0cQ>LJfTh5!nEFcCie!y07}X6cng1}f$} zq+|R*r6@wvXndhwQ&LBM=+WBL;PUk&yBY*Mc@SmZx7ha-{gkt5fO<-dnNdlzZ8CH0 z_HC@UT>H;BXgkWv$!lW$PIbBv^ICF=W<}|C8G%Buw^0A9j=y!d_y*4ZiL=5Bh+bj+ zJv=r%m1Q@$|0m?g4YRZ`S1Mw>6Fu+87#z9`CDi-suCuFXa1=Uy)`Z4MmHA`W?G zUDyBOPo-CBeqZ1uUrH47{yvvLcEugSK}zNRSjc^BKDb!kYU2%a->LX3OFGSOy35!I z;2yR|49X#g$`QcIaB^t3)135D=g?C-73Hmqs+f}2gB=iCjF9VSh?==DIHlSJ2oIr! zFY~!>9Io#ON@6ik#xZYt@5bgDc1xP zoYs6;a6Cu6eZ87_EK86xUeyAnO7C?`Xu@H3MH>H+I|E=+1;}syY28PLD4~Y`O9|<^ z<1ls+rXbx^KWCIX{oUB&4y$|N{p)4|s0r%VGC6MP^_R_kt(Fy{Fy<-wZ2#nafc%0B6HAfJ zcwc}twf_3i`BUMH?Ja-`0J%F9#Uu{&wcKP+9!eW4c)2MYcBhdy^=~mV!#f`Ka9b8# z?r*4iYjexe6=i+Oq)O2!)( ziZkT99`$O)grQk8XG{J5pCaeqdz?7w$)jf8qjAl@hX+H+u@MF&mk@VvtIVu@0~m?R zwL395khphor9ctTZC;wGE)p1_QOuMS<{_B%Aer}ZneP1)@MUlnBmqESE5(B8pAhqq z;MH}?AD1x`AK3b^{sZ-lQEnhF&!3x$UYfW@W7(4hdj8r|$PWc@=6`RPuj@wr_a~aI z1ErO%?=X;1AG*O35aG~ri~^XqZ#TKk1}#NH$pP#+nZAySA94UVowyj;?jM$2d~<1y z2-#;cH*(D)`>@*w%x9QrNS`1OZpRh*uT%pi`~L|h=&tQnN_J6e^6OW@IL(1b89%2($wy{YHe zf@5&?JX{=p4ftw<_zE_*{=h1VBp}IQecYSgsrRt<&9s7M#h#V*H9`!qdz7|QX-P$e ztc21U^=~-qrz|Op{~L~=fX|Nk0$sg8=(yMR&17)P(?c4usrD_Bgp}j}bbf|=%)Oz)(El*eJ7dmcxr2XX1po5Oma``fk60XiG7ztRJ9*vC zS6lpb!caxfgPC)F{tw4c#QpaF0Rn}~O5`mu?AqPg{$*cpf({0_)DTS42M3p8uNi;ng8aWSe&kUzL_=nfQ}ddGJHaXen5+HB4r49QAC9C888uVN zg{ym1VVbTBK%|t)u}TL$kDq~xmzoD)UG0zE#=q1ORp2~HX+~|-C}%uVv>R5l+UKu*;#}m;hv%o9KVyb16aeQ)D2<0Z&ImMJGmqE zuLom&dh=4SyfC2nvlA-*z!a^SCor&p`CXtC^2CoJv2~&E=slP4;kIvX$4rA>Sv-Oi3fyE=;v-ChH@sp16rXr=g_N1&@g-p;!X~5 z2F|^?hvJdhh&~f`DqX~Pn7)=Z>>ZVRy}hy*k`KN@&cQk0FN42N`||GE_b0G{I9fud zsSl8uCmTT29LVqn3sCF?^nh$!*l!f*vnCl>pU(A$m`~Kwc z8T9VY9R=hVQUhL}@|D*3{H{Qcgbz4&3^X?WhPW!pda{Z&ac5Fh?H5G+e*HqCxqGr%9 z%>AUB1K0Gm8VJKnc<_VfCu+O+Blfs$U) zZz=&)NLJ!55eUnK+ezDfa<@^h{z1&f7!(+-j2@UI2%TCZBVNv7yhW2BFi~awy7~bwAAenjL9P&r((j9L0tj+k$ z4J>GSyt{OodrQvu(yG_#SuvR+?ZzDu@+gfgoA) zBfJF#o!za~+uv)wx*8Bh4vZ!JwWkL-Q+>=O5qA{*$?R!Z!pzPnL6kICQK0cf4oGJ$ zLASIqP$2kifF9oxn#ZliFT4WTSz7{XBdpuUe?f}nDrDEAlY*A{XplT_BP*45d%jH; z84?Tbz)Z;qwsgfMMF2)3{JtKeO&Xj-UP-;lS03=?5Z*D?}ml+@Q0~4dXBW>#?nOvqPTb46KASR2OOr#9?eIYVpmh z#zQ(8ubi5CYQ5@c%l`?ty+y9I|jB zQNM*XYjSTeL>A@vjw2H%tvNTzGpS>J_|6q24j~g?9#_S}gvk~O_)e>2!3BSx!v&jUT->ZRK_hgaF4U*(j^eWIK@nf*N9jgra0sgk!dvEZS%rLxL9lZIy zUeE!I&S=+E_MxRDA9OR#C@b)aQnwxyZpJ{idsPgRMz3liaCjxp$>qbQ8(7U7n3}^K zjFFVJ4td;4g^65q!XE~0?JAKTGw`2wW@N0}2;8;7fcS*^m_`zrGVRLh!v_x&2myC# z3gICue&qDW$NA)XMa4vNR`9p0jXIPpl+E8W3;x$g+FiQC-o{5vG-D=MwfM_xDl? zHUuxmDBzQLcM!gSTZ@E&HF8h#I@POw zaZtE=a=oLOt{w`Z-Ph%56@k7W(sCB#H-Op|h<{Xwq7A+AOX=;3p57qvMxIGt z6N}Fbx8k3{z6Fw~9r9D(kx8rySp83wiHYX0n%;rGX>G)m3-RJeF>{mtBf|OGYiH-l z+>@_S4QH)Go!u=YR<0p!J2~tDp%iVi)5BtZ8%{MpTR`Y21T(7!+{)zEL1LMJegYc5 zY6Wn&HD-AW6ZoDGTPJL%D?vJ~=-8>;3F?SS0;{l6|{T$x*cwc>!fZgkiqhruJUF7zEc*vkwif zKU5OA-Vus|%f+eoHUUGXG>p4pi5wB#<)n2kR#hP_46QIHXpXt*)Q8OqE_zMWLkVu3 z66(Se)9R51Duf!`v|dInxt6G!(FR|gjv-Tk<&hnstL^stek1bT#Hi9y(8#}7+)3rp z?QKq~?9mATxqF>j)-ok%rqppL`9hikS?JvC-;G)tN1kE=$zyY&kf|{B=w7Tj{(Qe< z)j&o@uUbC1(JGoXr)nDqE}D5fr##5m|Lx=NBdY*^yA|_6CeJ(g8|?MWuER#bvyZxG zL5u`J$HSwV{!6*;3=(+{UbJL|)KTD<-!F05n*bQEf&-Bfuy)k+dijfQDbwh@n-3J9 z-QIU)Br=3$!444O!Tb|mkUY0GazQ{qKGpZ8=Z&vP-xr|GGk$1g z7;Y`)CQ&~3w{a4~xo4%&by?R7eP!!u75g>16u^2>-9vgp0{ognCUX9PvD+8C2}qvimKBVu2X&eut5joJw~3`2|FV-?)yXS#%FDxxoy;h|EW1;^1O*^V zR^P{{pba3Var7eC%C1K&L;1$764{W+8CDc9J_v2{AknQu)(MB(rDV5j$V1FmTQrMO zES|7|^yrL-^CwKp>ufP9 z4+I2Er3^|0*y`%8Y5GAHd%*M~$|nt#hIcTKd{>+VhUWO>FM3<=Qz_5a1@aulve=k?lh}B$GXX(?7k*+jb^nv464*2rvIWJ zl^%GBrAWJ_{%Y{E-~JXrl2lB@)3jp5(x!HCV(!I=i4R!!+E+0|JOKsTCh?t;y~my) zQuv%6euNgOT!q=ngi2-uf+DK3&jS)S0UT#3Qb$qu*iJ=vnnSdWjTc+12Pn@~AC#ws zoL1RG?dx~knzD;%=&vAq?VE@+=w^LTXWIy}qUc;VX%cgiwIe~fWOr9;>u5#7_z^pTLg(ZZi>r_ z5Iqf`ot`-{^zktju>nQ?dIsHXe*{_s1DqsR&$Z#~ot@$NRc9j=L#9!aGarGu2} z*f89|^S_27WisF?`Z>fY*z$ZKB~v!aKsWEO6rPN;K^J!=coY-0sk;iQOG1Qm(VyVN zM>8mSY3fO$CCzIwuZebQf^F+TQg%73J#%V{*@CawELjsP_ZhWhgA+scuw$a4BWBFD z_l>-{PtzwygQq+jUg%Jgrey!Y$w9Gu#!FL%vgc2IM6te%6?H{5A2P)ZL4(8?678bd zx8XeIhf5R$w!F^bD;@hl8D*F(O8;!LWbk6Nq64p>E3 zLEnc^8Q);kDs6qByKs=_vFuES)Pp=VJ1`D#6ib)+l+ujnTAZ5DP+v+G29<%|s@VM4 zJde@2z`X^3eW;~3#&E;c8V$CH&AApfhppM9ct@&(GxB86VW4mgUtyUWHmY6W?e%u? zbvjA7^0D^zWGp`YHX`Ly-8J6)g{;s9?IKv}PcRLbfgvb=%^;+@&{%!F$9!MH3vBsRVGG9%Yn3Z&~r2$gj4nuR|arpi!Ti@$b^I>>h%3 z0)tl$L%ZEsLUndn5nm64j`u=739(F2)j2`gue;0}M;O77>*Pr zKEs;Qj54#`ZlG{xwmr9fq;-W%@k#p${?UoE|7>pV#Kk0gxoJe1mP6UYcx6wufQUG;oT*=pCj0^xpeXG(|7#?zl1{M-Yo_sk z!v1gK+sI+Sv+^UcK85vzI}$c)%E#}~BNG!GCFVN0{g=axNVuZIKn+QOOa-8%* z$>xTUdxmGdMeXFBpQ}f;`1%iHxRt!Cp`Q~TSMHl~;eoat%ftw(HQruUsyRQJsqGI6 zEK_8jCFyFk={;!coPil{h3uc66N`U|mKXeAmhELI>rGDbRf*s~0?h8?3yJGcrb)nz z)Yfa+ZSCZmunxwuod+3@F)@SXHVqfP;822dYf7@EE+r26hG+Aei8+ue0Cq2PT+t#6 z&9VolCSW2E@K?k^5Cv|57uWISLc8v2C^7VbK~Oj%1k1dkWWBa znn2M67Qlwc?%45kspwR3es;7^hYN|7fv&b>07MrDRf8eIf8wlqrrf0LjdnIyk1eQ6 zz#e!2;zIFC9O$&cIt(5YFrC)~mx4W<+4@cl1Q?w;S(GsCJcn%s0vNYEGxCORvH-i<|B`8&z!jO!;u?yFLe5lKwb5lDO&!+)#*y?H>LFR5)*qnhru4C{59)G_pnCnay z_KW+uE7N)zr&P8rXr&p_@DO!R;Tq{}7XqC+ususrt3pn|`Z&&7irONL+qG|^op*Rq zE5xSM!>Zj1 zPXHw2gAu2S9kRmlZeCU(a$~D~yQakfxslpNe)s*o(cHiihb$FTGqTXbs4wI`HB+Zh zo<>^vof0$wnAM#&;)tdX|JR&$KQHw0kHV-`V_c*7r=NCpwuZRpuZ>?juo1qu#0kg};+ zp=Q`&+jaR-k)=uzb|A(2mUGOtC9ZCA`K5F6i`xk}iW0|gf>K$CsEc!QJWwleShHB{|B>|TTB1| literal 0 HcmV?d00001 diff --git a/apps/native/assets/splash.png b/apps/native/assets/splash.png deleted file mode 100644 index 0e89705a9436743e42954d3744a0e7ff0d3d4701..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47346 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 diff --git a/apps/native/modules/games_status/map_game_status_to_label.ts b/apps/native/modules/games_status/map_game_status_to_label.ts new file mode 100644 index 0000000..e67af6a --- /dev/null +++ b/apps/native/modules/games_status/map_game_status_to_label.ts @@ -0,0 +1,16 @@ +import { GameStatus } from "@/__generated__/types"; + +export const mapGameStatusToLabel = (status: GameStatus) => { + switch (status) { + case "BACKLOG": + return "Backlog"; + case "COMPLETED": + return "Ukończona"; + case "IN_PROGRESS": + return "W trakcie"; + case "RETIRED": + return "Porzucona"; + default: + return "Nieznany status"; + } +}; diff --git a/apps/native/modules/layouts/go_back_header/go_back_header.tsx b/apps/native/modules/layouts/go_back_header/go_back_header.tsx index fa1435b..6e2de43 100644 --- a/apps/native/modules/layouts/go_back_header/go_back_header.tsx +++ b/apps/native/modules/layouts/go_back_header/go_back_header.tsx @@ -1,12 +1,12 @@ -import { ArrowLeft } from "@tamagui/lucide-icons"; -import { router, useNavigation } from "expo-router"; +import { useRouter } from "expo-router"; +import { ArrowLeft } from "lucide-react-native"; import { View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { ButtonWithIcon } from "ui/forms/button_icon"; import { Text } from "ui/typography/text"; import { truncateString } from "../../strings/truncate_string"; +import { Button, ButtonIcon } from "@/ui/forms/button/button"; import { HStack } from "@/ui/layout/hstack/hstack"; type GoBackHeaderProps = { @@ -16,19 +16,15 @@ type GoBackHeaderProps = { export const GoBackHeader = ({ goBackUrl, text }: GoBackHeaderProps) => { const insets = useSafeAreaInsets(); - const navigation = useNavigation(); + const router = useRouter(); return ( <> - - - goBackUrl ? router.push(goBackUrl) : navigation.goBack() - } - icon={} - /> - + + + {truncateString(text, 20)} diff --git a/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx b/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx index b6ca86f..33be4c2 100644 --- a/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx +++ b/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx @@ -21,7 +21,7 @@ export const GamesStatusAddFormScreen = () => { const game = gameQuery.data.game; return ( - + ; -export type GamesStatusFiltersQuery = { __typename?: 'Query', gamesStatusSortOptions: { __typename?: 'SortOptionsDTO', sortOptions: Array<{ __typename?: 'SortOptions', id: string, field: string, label: string, order: string }> }, availableGamesStatusProgressStates: { __typename?: 'GameStatusProgressStateDTO', gameStatusProgressState: Array<{ __typename?: 'GameStatusProgressState', label: string, value: Types.GameStatus }> }, platforms: { __typename?: 'PlatformsDTO', platforms: Array<{ __typename?: 'PlatformDTO', id: number, name: string }> } }; +export type GamesStatusFiltersQuery = { __typename?: 'Query', gamesStatusSortOptions: { __typename?: 'SortOptionsDTO', sortOptions: Array<{ __typename?: 'SortOptions', id: string, field: string, label: string, order: string }> }, availableGamesStatusProgressStates: { __typename?: 'GameStatusProgressStateDTO', gameStatusProgressState: Array<{ __typename?: 'GameStatusProgressState', label: string, value: Types.GameStatus }> }, platforms: { __typename?: 'PlatformsDTO', platforms: Array<{ __typename?: 'Platform', id: number, name: string }> } }; export const GamesStatusFiltersDocument = gql` diff --git a/apps/native/modules/screens/homepage/home_screen.tsx b/apps/native/modules/screens/homepage/home_screen.tsx index 3507304..baaed13 100644 --- a/apps/native/modules/screens/homepage/home_screen.tsx +++ b/apps/native/modules/screens/homepage/home_screen.tsx @@ -1,3 +1,5 @@ +import { useState } from "react"; +import { RefreshControl } from "react-native-gesture-handler"; import { ScrollView } from "tamagui"; import { GText } from "ui/typography/text"; @@ -5,15 +7,31 @@ import { FriendsActivity } from "./friends_activity/friends_activity"; import { IncomingGamesCarousel } from "./incoming_games_carousel/incoming_games_carousel"; import { LastUpdatedGameStatus } from "@/modules/screens/homepage/last_updated/last_update"; +import { useRefetchLastEditedGames } from "@/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games"; import { Box } from "@/ui/layout/box/box"; import { Divider } from "@/ui/layout/divider/divider"; import { VStack } from "@/ui/layout/vstack/vstack"; export const HomeScreen = () => { + const [isRefreshing, setIsRefreshing] = useState(false); + const { refetchLastEditedGames } = useRefetchLastEditedGames(); + + const onRefresh = async () => { + setIsRefreshing(true); + await refetchLastEditedGames(); + setIsRefreshing(false); + }; + return ( <> - + + } + > diff --git a/apps/native/modules/screens/homepage/last_updated/last_update.tsx b/apps/native/modules/screens/homepage/last_updated/last_update.tsx index 06497fd..5610e60 100644 --- a/apps/native/modules/screens/homepage/last_updated/last_update.tsx +++ b/apps/native/modules/screens/homepage/last_updated/last_update.tsx @@ -1,38 +1,49 @@ +import { Link, useRouter } from "expo-router"; + +import { mapGameStatusToLabel } from "@/modules/games_status/map_game_status_to_label"; import { HomepageSection } from "@/modules/screens/homepage/homepage_section/homepage_section"; import { HomepageSectionCarousel } from "@/modules/screens/homepage/homepage_section/homepage_section_carousel"; +import { useLastEditedGames } from "@/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games"; import { truncateString } from "@/modules/strings/truncate_string"; +import { SkeletonText } from "@/ui/feedback/skeleton/skeleton"; +import { Button, ButtonText } from "@/ui/forms/button/button"; +import { Pressable } from "@/ui/forms/pressable/pressable"; import { Box } from "@/ui/layout/box/box"; import { VStack } from "@/ui/layout/vstack/vstack"; import { Image } from "@/ui/media_and_icons/image/image"; import { GText } from "@/ui/typography/text"; -const sampleData = [ - { - gameImage: "https://howlongtobeat.com/games/62941_Hades.jpg?width=760", - gameTitle: "Hades", - gameStatus: "W trakcie", - }, - { - gameImage: "https://howlongtobeat.com/games/62941_Hades.jpg?width=760", - gameTitle: "Cyberpunk 2077Cyberpunk 2077", - gameStatus: "Ukończona", - }, - { - gameImage: "https://howlongtobeat.com/games/62941_Hades.jpg?width=760", - gameTitle: "Gra 3", - gameStatus: "Porzucona", - }, - { - gameImage: "https://howlongtobeat.com/games/62941_Hades.jpg?width=760", - gameTitle: "Gra 4", - gameStatus: "Backlog", - }, -]; export const LastUpdatedGameStatus = () => { + const lastEditedGames = useLastEditedGames(); + if (lastEditedGames.loading) { + return ; + } + const data = + lastEditedGames.data?.lastEditedGames.map((game) => ({ + gameStatusId: game.id, + gameImage: game.cover?.bigUrl ?? "", + gameTitle: game.name, + gameStatus: mapGameStatusToLabel(game.status), + })) ?? []; + + if (true) { + return ( + + + Nie edytowałeś jeszcze żadnych gier. + + + + + + ); + } return ( @@ -41,6 +52,7 @@ export const LastUpdatedGameStatus = () => { type CurrentlyPlayingCarouselItemProps = { item: { + gameStatusId: number; gameImage: string; gameTitle: string; gameStatus: string; @@ -50,8 +62,14 @@ type CurrentlyPlayingCarouselItemProps = { const CurrentlyPlayingCarouselItem = ({ item, }: CurrentlyPlayingCarouselItemProps) => { + const router = useRouter(); + const onPress = () => { + router.push( + `/(app)/(authorized)/games_status/games_status_info/${item.gameStatusId}`, + ); + }; return ( - + {item.gameTitle} - + ); }; diff --git a/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated.ts b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated.ts new file mode 100644 index 0000000..fedb176 --- /dev/null +++ b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated.ts @@ -0,0 +1,58 @@ +import * as Types from '../../../../../__generated__/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type LastEditedGamesQueryVariables = Types.Exact<{ + limit: Types.Scalars['Int']['input']; +}>; + + +export type LastEditedGamesQuery = { __typename?: 'Query', lastEditedGames: Array<{ __typename?: 'LastEditedGamesStatusDTO', id: number, name: string, status: Types.GameStatus, cover?: { __typename?: 'CoverDTO', bigUrl: string } | null }> }; + + +export const LastEditedGamesDocument = gql` + query LastEditedGames($limit: Int!) { + lastEditedGames(limit: $limit) { + id + name + status + cover { + bigUrl + } + } +} + `; + +/** + * __useLastEditedGamesQuery__ + * + * To run a query within a React component, call `useLastEditedGamesQuery` and pass it any options that fit your needs. + * When your component renders, `useLastEditedGamesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useLastEditedGamesQuery({ + * variables: { + * limit: // value for 'limit' + * }, + * }); + */ +export function useLastEditedGamesQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(LastEditedGamesDocument, options); + } +export function useLastEditedGamesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(LastEditedGamesDocument, options); + } +export function useLastEditedGamesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(LastEditedGamesDocument, options); + } +export type LastEditedGamesQueryHookResult = ReturnType; +export type LastEditedGamesLazyQueryHookResult = ReturnType; +export type LastEditedGamesSuspenseQueryHookResult = ReturnType; +export type LastEditedGamesQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.graphql b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.graphql new file mode 100644 index 0000000..51753ff --- /dev/null +++ b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.graphql @@ -0,0 +1,10 @@ +query LastEditedGames($limit: Int!) { + lastEditedGames(limit: $limit) { + id + name + status + cover { + bigUrl + } + } +} \ No newline at end of file diff --git a/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games.ts b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games.ts new file mode 100644 index 0000000..c3b5ef3 --- /dev/null +++ b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games.ts @@ -0,0 +1,18 @@ +import { useLastEditedGamesQuery } from "@/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated"; +export const useLastEditedGames = () => { + return useLastEditedGamesQuery({ + variables: { + limit: 10, + }, + }); +}; + +export const useRefetchLastEditedGames = () => { + const query = useLastEditedGames(); + + const refetchLastEditedGames = async () => { + await query.refetch(); + }; + + return { refetchLastEditedGames }; +}; diff --git a/apps/native/modules/screens/user_game_status/user_game_status_screen.tsx b/apps/native/modules/screens/user_game_status/user_game_status_screen.tsx index cef27a1..cdadb6e 100644 --- a/apps/native/modules/screens/user_game_status/user_game_status_screen.tsx +++ b/apps/native/modules/screens/user_game_status/user_game_status_screen.tsx @@ -1,4 +1,4 @@ -import { useGlobalSearchParams, useLocalSearchParams } from "expo-router"; +import { useLocalSearchParams } from "expo-router"; import { Card, ScrollView, Separator, Spinner } from "tamagui"; import { Text } from "ui/typography/text"; @@ -12,10 +12,11 @@ import { UserGameStatusMainSection } from "./user_game_status_sections/user_game import { UserGameStatusPlatformSection } from "./user_game_status_sections/user_game_status_platform_section/user_game_status_platform_section"; import { UserGameStatusReviewSection } from "./user_game_status_sections/user_game_status_review_section/user_game_status_review_section"; import { UserGameStatusScoreSection } from "./user_game_status_sections/user_game_status_score_section/user_game_status_score_section"; -import { GameStatus } from "../../../__generated__/types"; -import { HStack } from "../../../ui/layout/hstack/hstack"; import { useSetHeaderTitle } from "../../router/use_set_header_title"; +import { GameStatus } from "@/__generated__/types"; +import { HStack } from "@/ui/layout/hstack/hstack"; + type UserGameStatusScreenProps = { redirect: { review: "friends" | "games"; @@ -33,7 +34,7 @@ export const UserGameStatusScreen = ({ gameStatusId: games_status_id, oauthId: oauth_id, }); - + console.log(userGameStatusQuery.data); useSetHeaderTitle(userGameStatusQuery.data?.userGameStatus?.game.name || ""); if (userGameStatusQuery.loading || !userGameStatusQuery.data) { return ( diff --git a/apps/native/modules/screens/user_game_status/user_game_status_sections/user_game_status_game_completion_section/user_game_status_game_completion_section.tsx b/apps/native/modules/screens/user_game_status/user_game_status_sections/user_game_status_game_completion_section/user_game_status_game_completion_section.tsx index 9e3eba2..17382ec 100644 --- a/apps/native/modules/screens/user_game_status/user_game_status_sections/user_game_status_game_completion_section/user_game_status_game_completion_section.tsx +++ b/apps/native/modules/screens/user_game_status/user_game_status_sections/user_game_status_game_completion_section/user_game_status_game_completion_section.tsx @@ -1,7 +1,7 @@ -import { XStack } from "tamagui"; import { Text } from "ui/typography/text"; -import { GameStatus } from "../../../../../__generated__/types"; +import { GameStatus } from "@/__generated__/types"; +import { HStack } from "@/ui/layout/hstack/hstack"; const parseStatus = (status: GameStatus) => { switch (status) { @@ -26,7 +26,7 @@ export const UserGameStatusGameCompletionSection = ({ gameStatus, }: UserGameStatusGameCompletionSectionProps) => { return ( - + Status: diff --git a/apps/native/tsconfig.json b/apps/native/tsconfig.json index 4a4044d..c9dd7c4 100644 --- a/apps/native/tsconfig.json +++ b/apps/native/tsconfig.json @@ -3,11 +3,26 @@ "compilerOptions": { "strict": true, "baseUrl": "./", - "include": ["./"], + "include": [ + "./" + ], "paths": { - "@/*": ["./*"], - "tailwind.config": ["./tailwind.config.js"] + "@/*": [ + "./*" + ], + "tailwind.config": [ + "./tailwind.config.js" + ] } }, - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ], + "include": [ + "**/*.ts", + "**/*.tsx", + ".expo/types/**/*.ts", + "expo-env.d.ts", + "nativewind-env.d.ts" + ] } \ No newline at end of file diff --git a/apps/native/ui/data-display/carousel.tsx b/apps/native/ui/data-display/carousel.tsx index 66427b5..87fee12 100644 --- a/apps/native/ui/data-display/carousel.tsx +++ b/apps/native/ui/data-display/carousel.tsx @@ -114,10 +114,6 @@ export const Carousel = ({ scrollEventThrottle={16} onScrollBeginDrag={onScrollBeginDrag} onScrollEndDrag={onScrollEndDrag} - contentContainerStyle={{ - paddingLeft: 16, - paddingRight: 16, - }} > {data.map((item, index) => ( Date: Sat, 31 Jan 2026 00:39:26 +0100 Subject: [PATCH 5/8] feat(native/api): implement upcoming games feature with caching and Polish date formatting --- apps/api/package.json | 2 + .../dtos => infrastructure/igdb}/igdb.dto.ts | 0 .../src/infrastructure/igdb/igdb.service.ts | 133 ++++++++++++++++++ .../date_and_time/time/timestamp_to_ms.ts | 3 + apps/api/src/modules/games/games.dto.ts | 30 ++++ apps/api/src/modules/games/games.module.ts | 15 +- apps/api/src/modules/games/games.resolver.ts | 17 ++- .../interfaces/games-provider.interface.ts | 7 + .../get_upcoming_games.handler.ts | 33 +++++ .../get_upcoming_games.query.ts | 9 ++ .../src/modules/igdb/dtos/igdb_games.dto.ts | 21 --- apps/api/src/modules/igdb/igdb.module.ts | 23 --- apps/api/src/modules/igdb/igdb.service.ts | 32 ----- .../igdb_auth/igbd_auth.axios_interceptor.ts | 28 ---- .../igdb/igdb_auth/igdb_auth.module.ts | 22 --- .../igdb/igdb_auth/igdb_auth.service.ts | 74 ---------- .../modules/igdb/models/igdb_game.model.ts | 67 --------- apps/api/src/modules/search/search.module.ts | 2 - apps/api/src/schema.gql | 15 ++ .../modules/dates/date_to_polish_locale.ts | 8 ++ .../modules/screens/homepage/home_screen.tsx | 7 +- .../incoming_games_carousel.tsx | 105 ++++++++++---- .../incoming_games.generated.ts | 58 ++++++++ .../use_incoming_games/incoming_games.graphql | 13 ++ .../use_incoming_games/use_incoming_games.ts | 5 + .../homepage/last_updated/last_update.tsx | 2 +- yarn.lock | 67 +++++++++ 27 files changed, 497 insertions(+), 301 deletions(-) rename apps/api/src/{modules/igdb/igdb_auth/dtos => infrastructure/igdb}/igdb.dto.ts (100%) create mode 100644 apps/api/src/infrastructure/igdb/igdb.service.ts create mode 100644 apps/api/src/modules/date_and_time/time/timestamp_to_ms.ts create mode 100644 apps/api/src/modules/games/interfaces/games-provider.interface.ts create mode 100644 apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.handler.ts create mode 100644 apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.query.ts delete mode 100644 apps/api/src/modules/igdb/dtos/igdb_games.dto.ts delete mode 100644 apps/api/src/modules/igdb/igdb.module.ts delete mode 100644 apps/api/src/modules/igdb/igdb.service.ts delete mode 100644 apps/api/src/modules/igdb/igdb_auth/igbd_auth.axios_interceptor.ts delete mode 100644 apps/api/src/modules/igdb/igdb_auth/igdb_auth.module.ts delete mode 100644 apps/api/src/modules/igdb/igdb_auth/igdb_auth.service.ts delete mode 100644 apps/api/src/modules/igdb/models/igdb_game.model.ts create mode 100644 apps/native/modules/dates/date_to_polish_locale.ts create mode 100644 apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.generated.ts create mode 100644 apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.graphql create mode 100644 apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/use_incoming_games.ts diff --git a/apps/api/package.json b/apps/api/package.json index 441fd6d..eacefa0 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -29,6 +29,7 @@ "@nestjs/apollo": "^12.0.11", "@nestjs/axios": "^3.0.1", "@nestjs/bull": "^10.0.1", + "@nestjs/cache-manager": "^3.1.0", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/cqrs": "11.0.2", @@ -43,6 +44,7 @@ "@types/user-agents": "^1.0.4", "axios": "^1.6.2", "bull": "^4.11.5", + "cache-manager": "^7.2.8", "cheerio": "^1.0.0-rc.12", "cloudinary": "^1.41.2", "dayjs": "^1.11.10", diff --git a/apps/api/src/modules/igdb/igdb_auth/dtos/igdb.dto.ts b/apps/api/src/infrastructure/igdb/igdb.dto.ts similarity index 100% rename from apps/api/src/modules/igdb/igdb_auth/dtos/igdb.dto.ts rename to apps/api/src/infrastructure/igdb/igdb.dto.ts diff --git a/apps/api/src/infrastructure/igdb/igdb.service.ts b/apps/api/src/infrastructure/igdb/igdb.service.ts new file mode 100644 index 0000000..9d26e34 --- /dev/null +++ b/apps/api/src/infrastructure/igdb/igdb.service.ts @@ -0,0 +1,133 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { ExternalGameDTO } from '../../modules/games/games.dto'; +import { firstValueFrom } from 'rxjs'; +import { AxiosResponse, isAxiosError } from 'axios'; +import { IGamesProvider } from '../../modules/games/interfaces/games-provider.interface'; +import { OAuthTokenDto } from './igdb.dto'; +import { timestampToMs } from '../../modules/date_and_time/time/timestamp_to_ms'; + +@Injectable() +export class IgdbService implements IGamesProvider { + private readonly logger = new Logger(IgdbService.name); + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) {} + + private accessToken: string | null = null; + private tokenExpiration: number = 0; + + async getUpcomingGames(limit: number): Promise { + const now = Math.floor(Date.now() / 1000); + + const query = ` + fields name, cover.url, screenshots.url, first_release_date, platforms.name, category; + + where + first_release_date > ${now} & + platforms = (48,167,49,169,130) & + cover != null & + game_type = (0, 2, 4, 8, 9) & + version_parent = null; + + sort first_release_date asc; + limit ${limit}; +`; + + const token = await this.getTokenFromOAuth(); + + try { + const { data } = await firstValueFrom( + this.httpService.post('https://api.igdb.com/v4/games', query, { + headers: { + 'Client-ID': this.configService.get('IGDB_CLIENT_ID'), + Authorization: `Bearer ${token}`, + }, + }), + ); + this.logger.log(JSON.stringify(data, null, 2)); + return data.map(this.mapToDto); + } catch (e) { + this.logger.error('Error fetching games from IGDB', e); + return []; + } + } + + async getTokenFromOAuth(): Promise { + const now = Date.now(); + + if (this.accessToken && this.tokenExpiration > now + 3600 * 1000) { + return this.accessToken; + } + this.logger.log('Refreshing IGDB Access Token...'); + try { + const { data } = await firstValueFrom>( + this.httpService.post('https://id.twitch.tv/oauth2/token', null, { + params: { + client_id: this.configService.get('IGDB_CLIENT_ID'), + client_secret: this.configService.get('IGDB_CLIENT_SECRET'), + grant_type: 'client_credentials', + }, + }), + ); + this.accessToken = data.access_token; + this.tokenExpiration = now + timestampToMs(data.expires_in); + return this.accessToken; + } catch (e) { + if (isAxiosError(e)) { + this.logger.error( + 'Failed to authenticate with Twitch/IGDB', + e.response?.data || e.message, + ); + } + throw e; + } + } + + private mapToDto(game: IgdbGame): ExternalGameDTO { + return { + id: game.id.toString(), + name: game.name, + coverUrl: game.cover?.url + ? `https:${game.cover.url.replace('t_thumb', 't_cover_big')}` + : '', + backgroundUrl: game.screenshots?.[0]?.url + ? `https:${game.screenshots[0].url.replace( + 't_thumb', + 't_screenshot_big', + )}` + : '', + releaseDate: new Date(game.first_release_date * 1000), + platforms: + game.platforms?.map((p: any) => ({ + id: p.id.toString(), + name: p.name, + })) || [], + }; + } +} + +type IgdbGame = { + id: string; + cover: { + id: string; + url: string; + }; + first_release_date: number; + name: string; + platforms: IgdbGamePlatform[]; + screenshots: IgdbGameScreenshots[]; +}; + +type IgdbGamePlatform = { + id: string; + name: string; +}; + +type IgdbGameScreenshots = { + id: string; + url: string; +}; diff --git a/apps/api/src/modules/date_and_time/time/timestamp_to_ms.ts b/apps/api/src/modules/date_and_time/time/timestamp_to_ms.ts new file mode 100644 index 0000000..2937027 --- /dev/null +++ b/apps/api/src/modules/date_and_time/time/timestamp_to_ms.ts @@ -0,0 +1,3 @@ +export const timestampToMs = (timestamp: number): number => { + return timestamp * 1000; +}; diff --git a/apps/api/src/modules/games/games.dto.ts b/apps/api/src/modules/games/games.dto.ts index 94d1b41..da06d32 100644 --- a/apps/api/src/modules/games/games.dto.ts +++ b/apps/api/src/modules/games/games.dto.ts @@ -87,3 +87,33 @@ export class UpdateGameDataDTO { @Field(() => String) message: string; } + +@ObjectType() +export class ExternalGamePlatformDTO { + @Field() + id: string; + + @Field() + name: string; +} + +@ObjectType() +export class ExternalGameDTO { + @Field() + id: string; + + @Field() + name: string; + + @Field({ nullable: true }) + coverUrl?: string; + + @Field({ nullable: true }) + backgroundUrl?: string; // Np. screenshot + + @Field() + releaseDate: Date; + + @Field(() => [ExternalGamePlatformDTO]) + platforms: ExternalGamePlatformDTO[]; +} diff --git a/apps/api/src/modules/games/games.module.ts b/apps/api/src/modules/games/games.module.ts index b879e45..10b6c32 100644 --- a/apps/api/src/modules/games/games.module.ts +++ b/apps/api/src/modules/games/games.module.ts @@ -10,8 +10,15 @@ import { AuthModule } from '../auth/auth.module'; import { CommandHandlerType, CqrsModule, QueryHandlerType } from '@nestjs/cqrs'; import { GetGamesQueryHandler } from './queries/get_games/get_games.handler'; import { UpdateGameDataHandler } from './commands/update_game_data/update_game_data.handler'; +import { IgdbService } from '../../infrastructure/igdb/igdb.service'; +import { HttpModule } from '@nestjs/axios'; +import { GetUpcomingGamesHandler } from './queries/get_upcoming_games/get_upcoming_games.handler'; +import { CacheModule } from '@nestjs/cache-manager'; -const QueryHandlers: QueryHandlerType[] = [GetGamesQueryHandler]; +const QueryHandlers: QueryHandlerType[] = [ + GetGamesQueryHandler, + GetUpcomingGamesHandler, +]; const CommandHandlers: CommandHandlerType[] = [UpdateGameDataHandler]; @Module({ @@ -21,6 +28,8 @@ const CommandHandlers: CommandHandlerType[] = [UpdateGameDataHandler]; HowLongToBeatParserModule, AuthModule, CqrsModule, + HttpModule, + CacheModule.register(), ], providers: [ GamesService, @@ -29,6 +38,10 @@ const CommandHandlers: CommandHandlerType[] = [UpdateGameDataHandler]; GamesResolver, ...QueryHandlers, ...CommandHandlers, + { + provide: 'GAMES_PROVIDER', + useClass: IgdbService, + }, ], exports: [GamesService], }) diff --git a/apps/api/src/modules/games/games.resolver.ts b/apps/api/src/modules/games/games.resolver.ts index 3fe66ff..44fbc79 100644 --- a/apps/api/src/modules/games/games.resolver.ts +++ b/apps/api/src/modules/games/games.resolver.ts @@ -1,6 +1,7 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { GamesService } from './games.service'; import { + ExternalGameDTO, GameWithAllDataDTO, GetPaginatedGamesArgs, PaginatedGamesDTO, @@ -9,10 +10,15 @@ import { import { UseGuards } from '@nestjs/common'; import { AdminUserGuard } from '../auth/guards/admin-user.guard'; import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { QueryBus } from '@nestjs/cqrs'; +import { GetUpcomingGamesQuery } from './queries/get_upcoming_games/get_upcoming_games.query'; @Resolver() export class GamesResolver { - constructor(private readonly gamesService: GamesService) {} + constructor( + private readonly gamesService: GamesService, + private readonly queryBus: QueryBus, + ) {} @Query(() => GameWithAllDataDTO, { name: 'game' }) async getGameById(@Args('hltbId') hltbId: number) { @@ -37,4 +43,13 @@ export class GamesResolver { ): Promise { return this.gamesService.updateGameData(hltbId); } + + @Query(() => [ExternalGameDTO], { name: 'upcomingGames' }) + async getUpcomingGames( + @Args('limit') limit: number, + ): Promise { + return this.queryBus.execute( + new GetUpcomingGamesQuery(limit), + ); + } } diff --git a/apps/api/src/modules/games/interfaces/games-provider.interface.ts b/apps/api/src/modules/games/interfaces/games-provider.interface.ts new file mode 100644 index 0000000..f4b2adb --- /dev/null +++ b/apps/api/src/modules/games/interfaces/games-provider.interface.ts @@ -0,0 +1,7 @@ +import { ExternalGameDTO } from '../games.dto'; + +export interface IGamesProvider { + getUpcomingGames(limit: number): Promise; +} + +export const GAMES_PROVIDER = 'GAMES_PROVIDER'; diff --git a/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.handler.ts b/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.handler.ts new file mode 100644 index 0000000..0c46b58 --- /dev/null +++ b/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.handler.ts @@ -0,0 +1,33 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { GetUpcomingGamesQuery } from './get_upcoming_games.query'; +import { Inject } from '@nestjs/common'; +import { + GAMES_PROVIDER, + IGamesProvider, +} from '../../interfaces/games-provider.interface'; +import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager'; + +const CACHE_TTL_SECONDS = 3600; // 1 hour + +@QueryHandler(GetUpcomingGamesQuery) +export class GetUpcomingGamesHandler + implements IQueryHandler +{ + constructor( + @Inject(GAMES_PROVIDER) private readonly gamesProvider: IGamesProvider, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, + ) {} + + async execute({ limit }: GetUpcomingGamesQuery) { + const cacheKey = `upcoming_games_${limit}`; + const cachedData = await this.cacheManager.get(cacheKey); + + if (cachedData) { + return cachedData; + } + + const games = await this.gamesProvider.getUpcomingGames(limit); + await this.cacheManager.set(cacheKey, games, CACHE_TTL_SECONDS); + return games; + } +} diff --git a/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.query.ts b/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.query.ts new file mode 100644 index 0000000..f8fe04e --- /dev/null +++ b/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.query.ts @@ -0,0 +1,9 @@ +import { Query } from '@nestjs/cqrs'; + +export class GetUpcomingGamesQuery extends Query { + constructor(public readonly limit: number) { + super(); + } +} + +export type GetUpcomingGamesQueryResponse = any; diff --git a/apps/api/src/modules/igdb/dtos/igdb_games.dto.ts b/apps/api/src/modules/igdb/dtos/igdb_games.dto.ts deleted file mode 100644 index b7ac253..0000000 --- a/apps/api/src/modules/igdb/dtos/igdb_games.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod'; - -export const igdbGamesSchema = z.array( - z.object({ - id: z.number(), - cover: z.object({ id: z.number(), game: z.number(), url: z.string() }), - first_release_date: z.number().optional(), - genres: z - .array(z.object({ name: z.string(), slug: z.string(), id: z.number() })) - .optional(), - name: z.string(), - slug: z.string(), - url: z.string(), - platforms: z - .array(z.object({ name: z.string(), id: z.number(), slug: z.string() })) - .nullish(), - parent_game: z.number().nullish(), - }), -); - -export type IGDBGamesDto = z.infer; diff --git a/apps/api/src/modules/igdb/igdb.module.ts b/apps/api/src/modules/igdb/igdb.module.ts deleted file mode 100644 index aea3588..0000000 --- a/apps/api/src/modules/igdb/igdb.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HttpModule } from '@nestjs/axios'; -import { IgdbService } from './igdb.service'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { DatabaseModule } from '../database/database.module'; -import { IgdbAuthModule } from './igdb_auth/igdb_auth.module'; - -@Module({ - imports: [ - HttpModule.registerAsync({ - useFactory: async (configService: ConfigService) => ({ - baseURL: configService.get('IGDB_BASE_API_URL'), - }), - inject: [ConfigService], - }), - ConfigModule, - DatabaseModule, - IgdbAuthModule, - ], - providers: [IgdbService], - exports: [IgdbService], -}) -export class IgdbModule {} diff --git a/apps/api/src/modules/igdb/igdb.service.ts b/apps/api/src/modules/igdb/igdb.service.ts deleted file mode 100644 index cb6a3ea..0000000 --- a/apps/api/src/modules/igdb/igdb.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { firstValueFrom } from 'rxjs'; -import { ConfigService } from '@nestjs/config'; -import { igdbGamesSchema } from './dtos/igdb_games.dto'; -import { IgdbAuthService } from './igdb_auth/igdb_auth.service'; - -@Injectable() -export class IgdbService { - constructor( - private readonly httpService: HttpService, - private readonly configService: ConfigService, - private readonly igdbAuthService: IgdbAuthService, - ) {} - - async getGamesBySearch(search: string) { - const token = await this.igdbAuthService.getTokenFromDatabase(); - const { data } = await firstValueFrom( - this.httpService.post( - '/games', - `search "${search}"; fields name,first_release_date,slug,cover.*,release_dates.date, genres.name, genres.slug, url, id, slug, platforms.name, platforms.slug; limit 5;`, - { - headers: { - 'Client-ID': this.configService.get('IGDB_CLIENT_ID'), - Authorization: `Bearer ${token}`, - }, - }, - ), - ); - return igdbGamesSchema.safeParse(data); - } -} diff --git a/apps/api/src/modules/igdb/igdb_auth/igbd_auth.axios_interceptor.ts b/apps/api/src/modules/igdb/igdb_auth/igbd_auth.axios_interceptor.ts deleted file mode 100644 index 239bfaa..0000000 --- a/apps/api/src/modules/igdb/igdb_auth/igbd_auth.axios_interceptor.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { - AxiosInterceptor, - AxiosRejectedInterceptor, -} from '@narando/nest-axios-interceptor'; -import { isAxiosError } from 'axios'; -import { IgdbAuthService } from './igdb_auth.service'; - -@Injectable() -export class IgdbAxiosInterceptor extends AxiosInterceptor { - private readonly logger = new Logger(IgdbAxiosInterceptor.name); - constructor( - httpService: HttpService, - private readonly igdbAuthService: IgdbAuthService, - ) { - super(httpService); - } - - protected responseRejected(): AxiosRejectedInterceptor { - return async (error) => { - if (isAxiosError(error) && error.response?.status === 401) { - this.logger.error(error.response.data); - await this.igdbAuthService.getTokenFromOAuth(); - } - }; - } -} diff --git a/apps/api/src/modules/igdb/igdb_auth/igdb_auth.module.ts b/apps/api/src/modules/igdb/igdb_auth/igdb_auth.module.ts deleted file mode 100644 index a0a06b0..0000000 --- a/apps/api/src/modules/igdb/igdb_auth/igdb_auth.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HttpModule } from '@nestjs/axios'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { DatabaseModule } from '../../database/database.module'; -import { IgdbAuthService } from './igdb_auth.service'; -import { IgdbAxiosInterceptor } from './igbd_auth.axios_interceptor'; - -@Module({ - imports: [ - HttpModule.registerAsync({ - useFactory: async (configService: ConfigService) => ({ - baseURL: configService.get('IGDB_AUTH0_BASE_URL'), - }), - inject: [ConfigService], - }), - ConfigModule, - DatabaseModule, - ], - providers: [IgdbAxiosInterceptor, IgdbAuthService], - exports: [IgdbAuthService], -}) -export class IgdbAuthModule {} diff --git a/apps/api/src/modules/igdb/igdb_auth/igdb_auth.service.ts b/apps/api/src/modules/igdb/igdb_auth/igdb_auth.service.ts deleted file mode 100644 index b464bd3..0000000 --- a/apps/api/src/modules/igdb/igdb_auth/igdb_auth.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { ConfigService } from '@nestjs/config'; -import { PrismaService } from '../../database/prisma.service'; -import { OAuthTokenDto } from './dtos/igdb.dto'; -import { firstValueFrom } from 'rxjs'; -import { AxiosResponse } from 'axios'; - -@Injectable() -export class IgdbAuthService { - constructor( - private readonly httpService: HttpService, - private readonly configService: ConfigService, - private readonly prismaService: PrismaService, - ) {} - - async getTokenFromOAuth(): Promise { - const { data } = await firstValueFrom>( - this.httpService.post( - '/token', - { - client_id: this.configService.get('IGDB_CLIENT_ID'), - client_secret: this.configService.get('IGDB_CLIENT_SECRET'), - grant_type: 'client_credentials', - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ), - ); - if (data.access_token) { - const token = await this.getTokenFromDatabase(); - if (token) { - await this.updateTokenInDatabase(data.access_token); - } else { - await this.saveTokenToDatabase(data.access_token); - } - } - return data; - } - - async updateTokenInDatabase( - token: OAuthTokenDto['access_token'], - ): Promise { - await this.prismaService.iGBDBAuth.update({ - where: { - id: 1, - }, - data: { - token, - }, - }); - } - - async getTokenFromDatabase(): Promise { - const data = await this.prismaService.iGBDBAuth.findFirst(); - if (!data) { - return null; - } - return data.token; - } - - async saveTokenToDatabase( - token: OAuthTokenDto['access_token'], - ): Promise { - await this.prismaService.iGBDBAuth.create({ - data: { - token, - }, - }); - } -} diff --git a/apps/api/src/modules/igdb/models/igdb_game.model.ts b/apps/api/src/modules/igdb/models/igdb_game.model.ts deleted file mode 100644 index f414a40..0000000 --- a/apps/api/src/modules/igdb/models/igdb_game.model.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; - -@ObjectType() -class Cover { - @Field(() => ID) - id: string; - - @Field() - url: string; -} - -@ObjectType() -class Genre { - @Field(() => ID) - id: string; - - @Field() - name: string; - - @Field() - slug: string; - - @Field() - igdbId: number; -} - -@ObjectType() -class Platform { - @Field(() => ID) - id: string; - - @Field() - name: string; - - @Field() - igdbId: number; - - @Field() - slug: string; -} - -@ObjectType() -export class IgdbGame { - @Field(() => ID) - id: string; - - @Field() - name: string; - - @Field(() => Cover) - cover: Cover; - - @Field({ nullable: true }) - first_release_date: number; - - @Field(() => [Genre], { nullable: true }) - genres: Genre[]; - - @Field(() => [Platform], { nullable: true }) - platforms: Platform[]; - - @Field() - slug: string; - - @Field() - url: string; -} diff --git a/apps/api/src/modules/search/search.module.ts b/apps/api/src/modules/search/search.module.ts index e3908b4..941b1dd 100644 --- a/apps/api/src/modules/search/search.module.ts +++ b/apps/api/src/modules/search/search.module.ts @@ -1,5 +1,4 @@ import { forwardRef, Module } from '@nestjs/common'; -import { IgdbModule } from '../igdb/igdb.module'; import { DatabaseModule } from '../database/database.module'; import { SearchResolver } from './search.resolver'; import { SearchService } from './search.service'; @@ -15,7 +14,6 @@ const handlers = [FetchGamesFromHltbQueryHandler]; imports: [ CqrsModule, ConfigModule, - forwardRef(() => IgdbModule), forwardRef(() => DatabaseModule), GamesModule, forwardRef(() => HowLongToBeatParserModule), diff --git a/apps/api/src/schema.gql b/apps/api/src/schema.gql index 733d6c2..a298c81 100644 --- a/apps/api/src/schema.gql +++ b/apps/api/src/schema.gql @@ -52,6 +52,20 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime +type ExternalGameDTO { + backgroundUrl: String + coverUrl: String + id: String! + name: String! + platforms: [ExternalGamePlatformDTO!]! + releaseDate: DateTime! +} + +type ExternalGamePlatformDTO { + id: String! + name: String! +} + input FiltersGameStatus { achievementsCompleted: String! platform: String! @@ -316,6 +330,7 @@ type Query { profileInfo: ProfileInfoDTO! roles: [RoleDTO!]! search(input: String!): SearchResult! + upcomingGames(limit: Float!): [ExternalGameDTO!]! user(oauthId: String!): UserDataDTO! """Query to get user"s friend games statuses""" diff --git a/apps/native/modules/dates/date_to_polish_locale.ts b/apps/native/modules/dates/date_to_polish_locale.ts new file mode 100644 index 0000000..6c6a546 --- /dev/null +++ b/apps/native/modules/dates/date_to_polish_locale.ts @@ -0,0 +1,8 @@ +export const formatReleaseDateToPolishLocale = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString("pl-PL", { + day: "numeric", + month: "long", + year: "numeric", + }); +}; diff --git a/apps/native/modules/screens/homepage/home_screen.tsx b/apps/native/modules/screens/homepage/home_screen.tsx index baaed13..3687986 100644 --- a/apps/native/modules/screens/homepage/home_screen.tsx +++ b/apps/native/modules/screens/homepage/home_screen.tsx @@ -37,10 +37,9 @@ export const HomeScreen = () => { - - Nadchodzące premiery - - + + + diff --git a/apps/native/modules/screens/homepage/incoming_games_carousel/incoming_games_carousel.tsx b/apps/native/modules/screens/homepage/incoming_games_carousel/incoming_games_carousel.tsx index 6ff1583..0aa4cc8 100644 --- a/apps/native/modules/screens/homepage/incoming_games_carousel/incoming_games_carousel.tsx +++ b/apps/native/modules/screens/homepage/incoming_games_carousel/incoming_games_carousel.tsx @@ -1,31 +1,86 @@ -import { Text, Image } from "tamagui"; - -import { INCOMING_GAMES_MOCK } from "@/mocks/incoming_games_mock"; -import { Carousel } from "@/ui/data-display/carousel"; +import { formatReleaseDateToPolishLocale } from "@/modules/dates/date_to_polish_locale"; +import { HomepageSection } from "@/modules/screens/homepage/homepage_section/homepage_section"; +import { HomepageSectionCarousel } from "@/modules/screens/homepage/homepage_section/homepage_section_carousel"; +import { IncomingGamesQuery } from "@/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.generated"; +import { useIncomingGames } from "@/modules/screens/homepage/incoming_games_carousel/use_incoming_games/use_incoming_games"; +import { truncateString } from "@/modules/strings/truncate_string"; import { VStack } from "@/ui/layout/vstack/vstack"; +import { Image } from "@/ui/media_and_icons/image/image"; +import { GText } from "@/ui/typography/text"; export const IncomingGamesCarousel = () => { + const { data } = useIncomingGames(); + const games = data?.upcomingGames.map(mapIncomingGame); + + if (!games || games?.length === 0) { + return null; + } + return ( + + + + ); +}; + +const mapIncomingGame = (game: IncomingGamesQuery["upcomingGames"][number]) => { + return { + gameTitle: game.name, + gameReleaseDate: formatReleaseDateToPolishLocale(game.releaseDate), + gameImage: game.coverUrl || game.backgroundUrl || "", + }; +}; + +type IncomingGamesCarouselItemProps = { + item: { + gameTitle: string; + gameReleaseDate: string; + gameImage: string; + }; +}; + +const IncomingGamesCarouselItem = ({ + item, +}: IncomingGamesCarouselItemProps) => { return ( - game.game?.cover?.url)} - renderItem={({ item }) => { - return ( - - - {item.game.name} - - ); - }} - /> + + {item.gameTitle} + + + {truncateString(item.gameTitle, 15)} + + + {item.gameReleaseDate} + + + ); }; diff --git a/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.generated.ts b/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.generated.ts new file mode 100644 index 0000000..d05fff3 --- /dev/null +++ b/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.generated.ts @@ -0,0 +1,58 @@ +import * as Types from '../../../../../__generated__/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type IncomingGamesQueryVariables = Types.Exact<{ [key: string]: never; }>; + + +export type IncomingGamesQuery = { __typename?: 'Query', upcomingGames: Array<{ __typename?: 'ExternalGameDTO', id: string, name: string, coverUrl?: string | null, backgroundUrl?: string | null, releaseDate: any, platforms: Array<{ __typename?: 'ExternalGamePlatformDTO', id: string, name: string }> }> }; + + +export const IncomingGamesDocument = gql` + query IncomingGames { + upcomingGames(limit: 20) { + id + name + platforms { + id + name + } + coverUrl + backgroundUrl + releaseDate + } +} + `; + +/** + * __useIncomingGamesQuery__ + * + * To run a query within a React component, call `useIncomingGamesQuery` and pass it any options that fit your needs. + * When your component renders, `useIncomingGamesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useIncomingGamesQuery({ + * variables: { + * }, + * }); + */ +export function useIncomingGamesQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(IncomingGamesDocument, options); + } +export function useIncomingGamesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(IncomingGamesDocument, options); + } +export function useIncomingGamesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(IncomingGamesDocument, options); + } +export type IncomingGamesQueryHookResult = ReturnType; +export type IncomingGamesLazyQueryHookResult = ReturnType; +export type IncomingGamesSuspenseQueryHookResult = ReturnType; +export type IncomingGamesQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.graphql b/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.graphql new file mode 100644 index 0000000..ce510cb --- /dev/null +++ b/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.graphql @@ -0,0 +1,13 @@ +query IncomingGames { + upcomingGames(limit: 20) { + id + name + platforms { + id + name + } + coverUrl + backgroundUrl + releaseDate + } +} \ No newline at end of file diff --git a/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/use_incoming_games.ts b/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/use_incoming_games.ts new file mode 100644 index 0000000..c09d443 --- /dev/null +++ b/apps/native/modules/screens/homepage/incoming_games_carousel/use_incoming_games/use_incoming_games.ts @@ -0,0 +1,5 @@ +import { useIncomingGamesQuery } from "@/modules/screens/homepage/incoming_games_carousel/use_incoming_games/incoming_games.generated"; + +export const useIncomingGames = () => { + return useIncomingGamesQuery(); +}; diff --git a/apps/native/modules/screens/homepage/last_updated/last_update.tsx b/apps/native/modules/screens/homepage/last_updated/last_update.tsx index 5610e60..021f316 100644 --- a/apps/native/modules/screens/homepage/last_updated/last_update.tsx +++ b/apps/native/modules/screens/homepage/last_updated/last_update.tsx @@ -26,7 +26,7 @@ export const LastUpdatedGameStatus = () => { gameStatus: mapGameStatusToLabel(game.status), })) ?? []; - if (true) { + if (data.length === 0) { return ( diff --git a/yarn.lock b/yarn.lock index 24bdb1f..bd8491d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2719,6 +2719,16 @@ __metadata: languageName: node linkType: hard +"@cacheable/utils@npm:^2.3.3": + version: 2.3.3 + resolution: "@cacheable/utils@npm:2.3.3" + dependencies: + hashery: "npm:^1.3.0" + keyv: "npm:^5.5.5" + checksum: 10c0/aa6198dcbd799055465efcaa9c91b11266218bd674bc889665efe7a230ecbce17fda2502aed51bd65062f20ad3af464841a72a93e8609d579535736526e3988c + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -5479,6 +5489,13 @@ __metadata: languageName: node linkType: hard +"@keyv/serialize@npm:^1.1.1": + version: 1.1.1 + resolution: "@keyv/serialize@npm:1.1.1" + checksum: 10c0/b0008cae4a54400c3abf587b8cc2474c6f528ee58969ce6cf9cb07a04006f80c73c85971d6be6544408318a2bc40108236a19a82aea0a6de95aae49533317374 + languageName: node + linkType: hard + "@legendapp/motion@npm:^2.4.0": version: 2.5.3 resolution: "@legendapp/motion@npm:2.5.3" @@ -5713,6 +5730,19 @@ __metadata: languageName: node linkType: hard +"@nestjs/cache-manager@npm:^3.1.0": + version: 3.1.0 + resolution: "@nestjs/cache-manager@npm:3.1.0" + peerDependencies: + "@nestjs/common": ^9.0.0 || ^10.0.0 || ^11.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 || ^11.0.0 + cache-manager: ">=6" + keyv: ">=5" + rxjs: ^7.8.1 + checksum: 10c0/b8ebcaeae53425e4093ae2ec6bd3199bcdb22c7558810dc4c616c87617de4bbb1aca944c34de1dc14d5afb0b7bbda7223a449b39e0319df2a9e4c2953407b12b + languageName: node + linkType: hard + "@nestjs/cli@npm:^10.0.0": version: 10.2.1 resolution: "@nestjs/cli@npm:10.2.1" @@ -13542,6 +13572,7 @@ __metadata: "@nestjs/apollo": "npm:^12.0.11" "@nestjs/axios": "npm:^3.0.1" "@nestjs/bull": "npm:^10.0.1" + "@nestjs/cache-manager": "npm:^3.1.0" "@nestjs/cli": "npm:^10.0.0" "@nestjs/common": "npm:^10.0.0" "@nestjs/config": "npm:^3.1.1" @@ -13571,6 +13602,7 @@ __metadata: "@typescript-eslint/parser": "npm:^6.0.0" axios: "npm:^1.6.2" bull: "npm:^4.11.5" + cache-manager: "npm:^7.2.8" cheerio: "npm:^1.0.0-rc.12" cloudinary: "npm:^1.41.2" dayjs: "npm:^1.11.10" @@ -14727,6 +14759,16 @@ __metadata: languageName: node linkType: hard +"cache-manager@npm:^7.2.8": + version: 7.2.8 + resolution: "cache-manager@npm:7.2.8" + dependencies: + "@cacheable/utils": "npm:^2.3.3" + keyv: "npm:^5.5.5" + checksum: 10c0/d620f8489cbdd549574c5b42ce27eb5a99841af059b1ed0ffcceaa897e7f5093aff68186e1af306d125685f4e565ae96da2c3e632bb23b45938bdd5a01d5b7f3 + languageName: node + linkType: hard + "cacheable-lookup@npm:^5.0.3": version: 5.0.4 resolution: "cacheable-lookup@npm:5.0.4" @@ -19679,6 +19721,15 @@ __metadata: languageName: node linkType: hard +"hashery@npm:^1.3.0": + version: 1.4.0 + resolution: "hashery@npm:1.4.0" + dependencies: + hookified: "npm:^1.14.0" + checksum: 10c0/34e0c72f7eac78676ee81f7b4f8263cbc591d2eb66229c3fd8812d006362a43436038b07e07580d6cfa512954d674a01a045e0d4c6968cc13c1d817d638bfcbf + languageName: node + linkType: hard + "hasown@npm:^2.0.0": version: 2.0.0 resolution: "hasown@npm:2.0.0" @@ -19762,6 +19813,13 @@ __metadata: languageName: node linkType: hard +"hookified@npm:^1.14.0": + version: 1.15.0 + resolution: "hookified@npm:1.15.0" + checksum: 10c0/bb8e2b34d6e6c1a00fd946ebbb3988fd826ac1c7ae103507d3402ab94defdb09d1fafb37cec6eae48e60ba56217ef89b803b885ccf4156c149e499d05a7741ec + languageName: node + linkType: hard + "hosted-git-info@npm:^7.0.0": version: 7.0.2 resolution: "hosted-git-info@npm:7.0.2" @@ -21644,6 +21702,15 @@ __metadata: languageName: node linkType: hard +"keyv@npm:^5.5.5": + version: 5.6.0 + resolution: "keyv@npm:5.6.0" + dependencies: + "@keyv/serialize": "npm:^1.1.1" + checksum: 10c0/c3ea795b6e03593ca57c8f70928a69bad14c13389a7fb75649a115ff55615244b04d8902798d841c17f0bb4a8a8866c97133b543b93f151b440170bba09176db + languageName: node + linkType: hard + "kind-of@npm:^2.0.1": version: 2.0.1 resolution: "kind-of@npm:2.0.1" From 70456dbf317f2c2c52dd1f192d780c38b0e58c0f Mon Sep 17 00:00:00 2001 From: survikrowa Date: Sat, 31 Jan 2026 00:41:18 +0100 Subject: [PATCH 6/8] chore(api): remove debug logging of fetched game data --- apps/api/src/infrastructure/igdb/igdb.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/api/src/infrastructure/igdb/igdb.service.ts b/apps/api/src/infrastructure/igdb/igdb.service.ts index 9d26e34..e82ab91 100644 --- a/apps/api/src/infrastructure/igdb/igdb.service.ts +++ b/apps/api/src/infrastructure/igdb/igdb.service.ts @@ -48,7 +48,6 @@ export class IgdbService implements IGamesProvider { }, }), ); - this.logger.log(JSON.stringify(data, null, 2)); return data.map(this.mapToDto); } catch (e) { this.logger.error('Error fetching games from IGDB', e); From 9a8ad1853abca0ce486a38a13391087fd65fd38b Mon Sep 17 00:00:00 2001 From: survikrowa Date: Sun, 1 Feb 2026 20:02:31 +0100 Subject: [PATCH 7/8] feat(mobile): refactor FriendsActivity component to use HomepageSection and adjust layout in HomeScreen --- .../homepage/friends_activity/friends_activity.tsx | 10 +++------- apps/native/modules/screens/homepage/home_screen.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/native/modules/screens/homepage/friends_activity/friends_activity.tsx b/apps/native/modules/screens/homepage/friends_activity/friends_activity.tsx index 943e1e4..584c73e 100644 --- a/apps/native/modules/screens/homepage/friends_activity/friends_activity.tsx +++ b/apps/native/modules/screens/homepage/friends_activity/friends_activity.tsx @@ -1,11 +1,10 @@ import { useAuth0 } from "react-native-auth0"; -import { Text } from "ui/typography/text"; import { FriendsActivityLoading } from "./friends_activity_loading/friends_activity_loading"; import { useFriendsActivity } from "./use_friends_activity/use_friends_activity"; import { UserActivityCards } from "../../../user/user_activity/user_activity_cards/user_activity_cards"; -import { VStack } from "@/ui/layout/vstack/vstack"; +import { HomepageSection } from "@/modules/screens/homepage/homepage_section/homepage_section"; export const FriendsActivity = () => { const { friendsActivityQuery, getFriendsActivity } = useFriendsActivity(); @@ -15,12 +14,9 @@ export const FriendsActivity = () => { } const friendsActivity = getFriendsActivity(); return ( - - - Aktywność znajomych - + {friendsActivityQuery.loading && } {friendsActivity && } - + ); }; diff --git a/apps/native/modules/screens/homepage/home_screen.tsx b/apps/native/modules/screens/homepage/home_screen.tsx index 3687986..d264a08 100644 --- a/apps/native/modules/screens/homepage/home_screen.tsx +++ b/apps/native/modules/screens/homepage/home_screen.tsx @@ -37,11 +37,13 @@ export const HomeScreen = () => { - + - + + + From 744d53e45d1a1f6c470d068d56f96db81efbf71b Mon Sep 17 00:00:00 2001 From: survikrowa Date: Sun, 1 Feb 2026 21:21:38 +0100 Subject: [PATCH 8/8] feat(api): refactor authentication module to implement CQRS pattern and restructure file organization --- apps/api/plan-cqrsHexagonalRefactor.prompt.md | 74 +++++++++++++++++++ apps/api/src/libs/ddd/aggregate-root.base.ts | 3 + apps/api/src/libs/ddd/entity.base.ts | 17 +++++ apps/api/src/libs/ddd/repository.port.ts | 3 + .../create_user/create_user.command.ts | 0 .../create_user/create_user.handler.ts | 32 ++++++++ .../get_user_role/get_user_role.handler.ts | 2 +- .../get_user_role/get_user_role.query.ts | 0 apps/api/src/modules/auth/auth.module.ts | 23 ++++-- .../create_user/create_user.handler.ts | 51 ------------- .../auth/domain/models/auth-user.model.ts | 23 ++++++ .../auth/domain/ports/auth.repository.port.ts | 8 ++ .../adapters/prisma-auth.repository.ts | 59 +++++++++++++++ .../decorators}/auth.decorators.ts | 0 .../{ => infrastructure/graphql}/auth.dto.ts | 0 .../graphql}/auth.model.ts | 0 .../graphql}/auth.resolver.ts | 23 ++++-- .../guards/admin-user.guard.ts | 0 .../guards/auth-jwt.guard.ts | 9 ++- .../services}/auth.service.ts | 15 +--- .../strategies/o-auth-jwt-strategy.service.ts | 2 +- .../collections/collection.resolver.ts | 6 +- .../friends_activity.resolver.ts | 6 +- .../friends_list/friends_list.resolver.ts | 6 +- .../friends_requests.resolver.ts | 6 +- .../friends_search/friends_search.resolver.ts | 6 +- apps/api/src/modules/games/games.resolver.ts | 4 +- .../games_status/games_status.resolver.ts | 8 +- .../howlongtobeat_migration.controller.ts | 4 +- ...howlongtobeat_migration_status.resolver.ts | 6 +- .../src/modules/images/images.controller.ts | 2 +- .../modules/platforms/platforms.resolver.ts | 4 +- .../src/modules/profiles/profiles.resolver.ts | 6 +- apps/api/src/modules/roles/roles.resolver.ts | 4 +- .../modules/user_stats/user_stats.resolver.ts | 6 +- apps/api/src/modules/users/users.resolver.ts | 4 +- .../modules/screens/homepage/home_screen.tsx | 6 +- 37 files changed, 304 insertions(+), 124 deletions(-) create mode 100644 apps/api/plan-cqrsHexagonalRefactor.prompt.md create mode 100644 apps/api/src/libs/ddd/aggregate-root.base.ts create mode 100644 apps/api/src/libs/ddd/entity.base.ts create mode 100644 apps/api/src/libs/ddd/repository.port.ts rename apps/api/src/modules/auth/{ => application}/commands/create_user/create_user.command.ts (100%) create mode 100644 apps/api/src/modules/auth/application/commands/create_user/create_user.handler.ts rename apps/api/src/modules/auth/{ => application}/queries/get_user_role/get_user_role.handler.ts (91%) rename apps/api/src/modules/auth/{ => application}/queries/get_user_role/get_user_role.query.ts (100%) delete mode 100644 apps/api/src/modules/auth/commands/create_user/create_user.handler.ts create mode 100644 apps/api/src/modules/auth/domain/models/auth-user.model.ts create mode 100644 apps/api/src/modules/auth/domain/ports/auth.repository.port.ts create mode 100644 apps/api/src/modules/auth/infrastructure/adapters/prisma-auth.repository.ts rename apps/api/src/modules/auth/{ => infrastructure/decorators}/auth.decorators.ts (100%) rename apps/api/src/modules/auth/{ => infrastructure/graphql}/auth.dto.ts (100%) rename apps/api/src/modules/auth/{ => infrastructure/graphql}/auth.model.ts (100%) rename apps/api/src/modules/auth/{ => infrastructure/graphql}/auth.resolver.ts (59%) rename apps/api/src/modules/auth/{ => infrastructure}/guards/admin-user.guard.ts (100%) rename apps/api/src/modules/auth/{ => infrastructure}/guards/auth-jwt.guard.ts (72%) rename apps/api/src/modules/auth/{ => infrastructure/services}/auth.service.ts (58%) rename apps/api/src/modules/auth/{ => infrastructure}/strategies/o-auth-jwt-strategy.service.ts (95%) diff --git a/apps/api/plan-cqrsHexagonalRefactor.prompt.md b/apps/api/plan-cqrsHexagonalRefactor.prompt.md new file mode 100644 index 0000000..272b02f --- /dev/null +++ b/apps/api/plan-cqrsHexagonalRefactor.prompt.md @@ -0,0 +1,74 @@ +## Plan: Refaktoryzacja API pod CQRS i Architekturę Heksagonalną + +Ten plan ma na celu przekształcenie obecnej architektury "N-Tier" w pełnoprawną Architekturę Heksagonalną (Ports & Adapters) z CQRS, aby odseparować logikę biznesową od frameworka i bazy danych. + +### Główne Założenia +* **Domain Layer (Jądro)**: Czyste klasy TypeScript (Encje, Value Objects). Zero zależności od NestJS, Prisma czy zewnętrznych bibliotek. +* **Application Layer**: Obsługa Use Cases (CQRS Handlers). Definiuje interfejsy (Porty) dla komunikacji ze światem zewnętrznym. +* **Infrastructure Layer**: Implementacje interfejsów (Adaptery). Tu żyje Prisma, Resolvery GraphQL, Kontrolery REST i konfiguracja modułów NestJS. + +### Struktura folderów (Nowy schemat) +Dla każdego modułu (np. `games`, `users`) zastosujemy strukturę: +``` +modules// +├── domain/ # WARSTWA DOMENY +│ ├── models/ # Encje (np. Game, User) i Value Objects +│ └── ports/ # Interfejsy repozytoriów i serwisów zewnętrznych +├── application/ # WARSTWA APLIKACJI +│ ├── commands/ # Komendy i ich Handlery (WRITE model) +│ └── queries/ # Zapytania i ich Handlery (READ model) +└── infrastructure/ # WARSTWA INFRASTRUKTURY + ├── adapters/ # Implementacje portów (np. PrismaGameRepository) + ├── graphql/ # Resolvery i DTO (Inputs/Args) + └── .module.ts # Konfiguracja modułu (DI) +``` + +### Steps + +#### Faza 1: Przygotowanie Fundamentów +1. **Utwórz współdzielone abstrakcje w `libs/ddd` (lub `shared`)** + * Stwórz klasy bazowe `AggregateRoot` i `Entity` dla domeny. + * Zdefiniuj typy generyczne dla `RepositoryPort`. + +#### Faza 2: Migracja Modułu `Games` (Jako Pilot) +To najbardziej złożony moduł, idealny do przetestowania wzorca. + +1. **Wyodrębnij Domenę (`domain/`)** + * Stwórz encję `Game` w `domain/models/game.model.ts`. Przenieś tam logikę biznesową (np. walidacje stanu, metody operujące na danych), która obecnie jest rozproszona w serwisach. + * Zdefiniuj interfejs `GameRepositoryPort` w `domain/ports/`. Powinien operować na encjach domeny, nie na typach Prisma. + +2. **Przenieś Warstwę Aplikacji (`application/`)** + * Przenieś obecne `commands/` i `queries/` do folderu `application/`. + * W `UpdateGameDataHandler`: + * Zamiast `PrismaService`, wstrzyknij `GameRepositoryPort`. + * Zamiast bezpośrednich operacji na DB: pobierz encję -> wykonaj metodę na encji -> zapisz encję. + +3. **Zbuduj Warstwę Infrastruktury (`infrastructure/`)** + * Stwórz `PrismaGameRepository` w `adapters/`, który implementuje `GameRepositoryPort`. + * To tutaj przenieś logikę mapowania z obiektów Prisma na Encje Domenowe (Mappery). + * Przenieś `GamesResolver` do `infrastructure/graphql/`. + +4. **Skonfiguruj Dependency Injection** + * W `GamesModule` zarejestruj implementację portu: `provide: GameRepositoryPort, useClass: PrismaGameRepository`. + +#### Faza 3: Migracja Modułu `Users` i `Profiles` +Powtórz proces dla zarządzania użytkownikami, co uporządkuje zależności Auth0 i seedowania. + +1. **Separacja Modeli** + * Stwórz domenowy model `User`, który posiada metody biznesowe (np. `updateProfile`, `addFriend`). + * Oddziel model bazy danych (Prisma User) od modelu domeny. + +2. **Adaptery Zewnętrzne** + * Dla integracji z Auth0 (User Management), stwórz port `IdentityServicePort` w domenie i adapter w infrastrukturze. + +#### Faza 4: CQRS - Rozdzielenie Modelu Odczytu (Read Model) +Dla zapytań (Queries), pełna heksagonalna "czystość" bywa uciążliwa (overhead przy mapowaniu). + +1. **Optymalizacja Queries** + * Zezwól Handlerom Query (`application/queries/`) na bezpośredni dostęp do `PrismaService` lub dedykowanych widoków (Raw SQL), pomijając Encje Domenowe na rzecz szybkich DTO (Read Models). + * Dla Command (zapis) **zawsze** używaj Repozytoriów i Encji. + +### Further Considerations +1. **Mappery**: Będziesz potrzebował mapperów (np. `GameMapper`), które konwertują `PrismaGame` <-> `DomainGame`. To wymaga napisania dodatkowego kodu, ale daje niezależność. +2. **Komunikacja między modułami**: Moduł `Games` nie powinien importować `UsersService`. Zamiast tego `Games` powinien zdefiniować port `OwnerServicePort`, a moduł `Users` powinien go zaimplementować (lub użyć QueryBus/CommandBus do komunikacji). +3. **Transakcje**: W architekturze heksagonalnej transakcje są trudniejsze. Rozważ użycie `UnitOfWork` lub mechanizmu transakcji NestJS (`ClsService`) w warstwie aplikacji. diff --git a/apps/api/src/libs/ddd/aggregate-root.base.ts b/apps/api/src/libs/ddd/aggregate-root.base.ts new file mode 100644 index 0000000..5e9040e --- /dev/null +++ b/apps/api/src/libs/ddd/aggregate-root.base.ts @@ -0,0 +1,3 @@ +import { Entity } from './entity.base'; + +export abstract class AggregateRoot extends Entity {} diff --git a/apps/api/src/libs/ddd/entity.base.ts b/apps/api/src/libs/ddd/entity.base.ts new file mode 100644 index 0000000..442614b --- /dev/null +++ b/apps/api/src/libs/ddd/entity.base.ts @@ -0,0 +1,17 @@ +export abstract class Entity { + protected readonly _id: number; + protected props: Props; + + constructor(props: Props, id?: number) { + this._id = id ? id : 0; + this.props = props; + } + + get id(): number { + return this._id; + } + + public getProps(): Props { + return { ...this.props }; + } +} diff --git a/apps/api/src/libs/ddd/repository.port.ts b/apps/api/src/libs/ddd/repository.port.ts new file mode 100644 index 0000000..698eb33 --- /dev/null +++ b/apps/api/src/libs/ddd/repository.port.ts @@ -0,0 +1,3 @@ +export interface RepositoryPort { + save(entity: Entity): Promise; +} diff --git a/apps/api/src/modules/auth/commands/create_user/create_user.command.ts b/apps/api/src/modules/auth/application/commands/create_user/create_user.command.ts similarity index 100% rename from apps/api/src/modules/auth/commands/create_user/create_user.command.ts rename to apps/api/src/modules/auth/application/commands/create_user/create_user.command.ts diff --git a/apps/api/src/modules/auth/application/commands/create_user/create_user.handler.ts b/apps/api/src/modules/auth/application/commands/create_user/create_user.handler.ts new file mode 100644 index 0000000..d7a1842 --- /dev/null +++ b/apps/api/src/modules/auth/application/commands/create_user/create_user.handler.ts @@ -0,0 +1,32 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { CreateUserCommand } from './create_user.command'; +import { Inject } from '@nestjs/common'; +import { + AUTH_REPOSITORY, + AuthRepositoryPort, +} from '../../../domain/ports/auth.repository.port'; +import { AuthUser, UserRole } from '../../../domain/models/auth-user.model'; + +@CommandHandler(CreateUserCommand) +export class CreateUserCommandHandler + implements ICommandHandler +{ + constructor( + @Inject(AUTH_REPOSITORY) + private readonly authRepository: AuthRepositoryPort, + ) {} + + async execute(command: CreateUserCommand) { + const newUser = AuthUser.create({ + oauthId: command.oauthId, + role: UserRole.USER, + }); + + const savedUser = await this.authRepository.save(newUser); + + return { + id: savedUser.id, + role: savedUser.role, + }; + } +} diff --git a/apps/api/src/modules/auth/queries/get_user_role/get_user_role.handler.ts b/apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.handler.ts similarity index 91% rename from apps/api/src/modules/auth/queries/get_user_role/get_user_role.handler.ts rename to apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.handler.ts index e952255..f873903 100644 --- a/apps/api/src/modules/auth/queries/get_user_role/get_user_role.handler.ts +++ b/apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.handler.ts @@ -1,6 +1,6 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; import { GetUserRoleQuery } from './get_user_role.query'; -import { PrismaService } from '../../../database/prisma.service'; +import { PrismaService } from '../../../../database/prisma.service'; @QueryHandler(GetUserRoleQuery) export class GetUserRoleQueryHandler diff --git a/apps/api/src/modules/auth/queries/get_user_role/get_user_role.query.ts b/apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.query.ts similarity index 100% rename from apps/api/src/modules/auth/queries/get_user_role/get_user_role.query.ts rename to apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.query.ts diff --git a/apps/api/src/modules/auth/auth.module.ts b/apps/api/src/modules/auth/auth.module.ts index 9de4918..671445e 100644 --- a/apps/api/src/modules/auth/auth.module.ts +++ b/apps/api/src/modules/auth/auth.module.ts @@ -1,15 +1,17 @@ import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; -import { OAuthJwtStrategy } from './strategies/o-auth-jwt-strategy.service'; -import { AuthResolver } from './auth.resolver'; +import { OAuthJwtStrategy } from './infrastructure/strategies/o-auth-jwt-strategy.service'; +import { AuthResolver } from './infrastructure/graphql/auth.resolver'; import { ConfigService } from '@nestjs/config'; -import { AuthService } from './auth.service'; +import { AuthService } from './infrastructure/services/auth.service'; import { DatabaseModule } from '../database/database.module'; import { BullModule } from '@nestjs/bull'; import { HttpModule } from '@nestjs/axios'; import { CqrsModule } from '@nestjs/cqrs'; -import { GetUserRoleQueryHandler } from './queries/get_user_role/get_user_role.handler'; -import { CreateUserCommandHandler } from './commands/create_user/create_user.handler'; +import { GetUserRoleQueryHandler } from './application/queries/get_user_role/get_user_role.handler'; +import { CreateUserCommandHandler } from './application/commands/create_user/create_user.handler'; +import { AUTH_REPOSITORY } from './domain/ports/auth.repository.port'; +import { PrismaAuthRepository } from './infrastructure/adapters/prisma-auth.repository'; const handlers = [GetUserRoleQueryHandler, CreateUserCommandHandler]; @@ -26,7 +28,16 @@ const handlers = [GetUserRoleQueryHandler, CreateUserCommandHandler]; inject: [ConfigService], }), ], - providers: [OAuthJwtStrategy, AuthResolver, AuthService, ...handlers], + providers: [ + OAuthJwtStrategy, + AuthResolver, + AuthService, + ...handlers, + { + provide: AUTH_REPOSITORY, + useClass: PrismaAuthRepository, + }, + ], exports: [PassportModule, AuthService], }) export class AuthModule {} diff --git a/apps/api/src/modules/auth/commands/create_user/create_user.handler.ts b/apps/api/src/modules/auth/commands/create_user/create_user.handler.ts deleted file mode 100644 index 7a0eb15..0000000 --- a/apps/api/src/modules/auth/commands/create_user/create_user.handler.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { CreateUserCommand } from './create_user.command'; -import { RoleEnum } from '@prisma/client'; -import { PrismaService } from '../../../database/prisma.service'; - -@CommandHandler(CreateUserCommand) -export class CreateUserCommandHandler - implements ICommandHandler -{ - constructor(private readonly prismaService: PrismaService) {} - - async execute(command: CreateUserCommand) { - const { id, role } = await this.createUser(command.oauthId); - return { - id, - role, - }; - } - - async createUser(oauthId: string) { - return this.prismaService.$transaction(async (prisma) => { - const userRole = await prisma.role.findFirst({ - where: { - name: RoleEnum.USER, - }, - }); - if (!userRole) { - throw new Error('Role not found'); - } - const user = await prisma.user.create({ - data: { - oauthId, - role: { - connectOrCreate: { - where: { - oauthId, - }, - create: { - roleId: userRole.id, - }, - }, - }, - }, - }); - return { - ...user, - role: userRole.name, - }; - }); - } -} diff --git a/apps/api/src/modules/auth/domain/models/auth-user.model.ts b/apps/api/src/modules/auth/domain/models/auth-user.model.ts new file mode 100644 index 0000000..f09e95a --- /dev/null +++ b/apps/api/src/modules/auth/domain/models/auth-user.model.ts @@ -0,0 +1,23 @@ +import { AggregateRoot } from '../../../../libs/ddd/aggregate-root.base'; +import { RoleEnum } from '@prisma/client'; + +export interface AuthUserProps { + oauthId: string; + role: RoleEnum; +} + +export class AuthUser extends AggregateRoot { + get oauthId(): string { + return this.props.oauthId; + } + + get role(): RoleEnum { + return this.props.role; + } + + static create(props: AuthUserProps): AuthUser { + return new AuthUser(props); + } +} + +export { RoleEnum as UserRole }; diff --git a/apps/api/src/modules/auth/domain/ports/auth.repository.port.ts b/apps/api/src/modules/auth/domain/ports/auth.repository.port.ts new file mode 100644 index 0000000..9011bdb --- /dev/null +++ b/apps/api/src/modules/auth/domain/ports/auth.repository.port.ts @@ -0,0 +1,8 @@ +import { RepositoryPort } from '../../../../libs/ddd/repository.port'; +import { AuthUser } from '../models/auth-user.model'; + +export const AUTH_REPOSITORY = Symbol('AUTH_REPOSITORY'); + +export interface AuthRepositoryPort extends RepositoryPort { + findUserByOauthId(oauthId: string): Promise; +} diff --git a/apps/api/src/modules/auth/infrastructure/adapters/prisma-auth.repository.ts b/apps/api/src/modules/auth/infrastructure/adapters/prisma-auth.repository.ts new file mode 100644 index 0000000..4e8496c --- /dev/null +++ b/apps/api/src/modules/auth/infrastructure/adapters/prisma-auth.repository.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../../database/prisma.service'; +import { AuthRepositoryPort } from '../../domain/ports/auth.repository.port'; +import { AuthUser, UserRole } from '../../domain/models/auth-user.model'; +import { RoleEnum } from '@prisma/client'; + +@Injectable() +export class PrismaAuthRepository implements AuthRepositoryPort { + constructor(private readonly prismaService: PrismaService) {} + + async save(user: AuthUser): Promise { + const roleName = + user.role === UserRole.ADMIN ? RoleEnum.ADMIN : RoleEnum.USER; + + const saved = await this.prismaService.$transaction(async (prisma) => { + const userRole = await prisma.role.findFirst({ + where: { name: roleName }, + }); + + if (!userRole) { + throw new Error('Role not found'); + } + + const newUser = await prisma.user.create({ + data: { + oauthId: user.oauthId, + role: { + connectOrCreate: { + where: { + oauthId: user.oauthId, + }, + create: { + roleId: userRole.id, + }, + }, + }, + }, + }); + return { ...newUser, roleName: userRole.name }; + }); + + const role = + saved.roleName === RoleEnum.ADMIN ? UserRole.ADMIN : UserRole.USER; + return new AuthUser({ oauthId: saved.oauthId, role }, saved.id); + } + + async findUserByOauthId(oauthId: string): Promise { + const user = await this.prismaService.user.findUnique({ + where: { oauthId }, + include: { role: { include: { role: true } } }, + }); + + if (!user || !user.role) return null; + + const roleName = user.role.role.name; + const role = roleName === RoleEnum.ADMIN ? UserRole.ADMIN : UserRole.USER; + return new AuthUser({ oauthId: user.oauthId, role }, user.id); + } +} diff --git a/apps/api/src/modules/auth/auth.decorators.ts b/apps/api/src/modules/auth/infrastructure/decorators/auth.decorators.ts similarity index 100% rename from apps/api/src/modules/auth/auth.decorators.ts rename to apps/api/src/modules/auth/infrastructure/decorators/auth.decorators.ts diff --git a/apps/api/src/modules/auth/auth.dto.ts b/apps/api/src/modules/auth/infrastructure/graphql/auth.dto.ts similarity index 100% rename from apps/api/src/modules/auth/auth.dto.ts rename to apps/api/src/modules/auth/infrastructure/graphql/auth.dto.ts diff --git a/apps/api/src/modules/auth/auth.model.ts b/apps/api/src/modules/auth/infrastructure/graphql/auth.model.ts similarity index 100% rename from apps/api/src/modules/auth/auth.model.ts rename to apps/api/src/modules/auth/infrastructure/graphql/auth.model.ts diff --git a/apps/api/src/modules/auth/auth.resolver.ts b/apps/api/src/modules/auth/infrastructure/graphql/auth.resolver.ts similarity index 59% rename from apps/api/src/modules/auth/auth.resolver.ts rename to apps/api/src/modules/auth/infrastructure/graphql/auth.resolver.ts index 500642b..175d4f4 100644 --- a/apps/api/src/modules/auth/auth.resolver.ts +++ b/apps/api/src/modules/auth/infrastructure/graphql/auth.resolver.ts @@ -1,15 +1,22 @@ import { Context, Query, Resolver } from '@nestjs/graphql'; -import { AuthService } from './auth.service'; +import { AuthService } from '../services/auth.service'; import { Prisma } from '@prisma/client'; import { UseGuards } from '@nestjs/common'; import { AuthUserVerification } from './auth.model'; -import { JwtAuthGuard } from './guards/auth-jwt.guard'; -import { User } from './auth.decorators'; +import { JwtAuthGuard } from '../guards/auth-jwt.guard'; +import { User } from '../decorators/auth.decorators'; import { UserAuthDTO } from './auth.dto'; +import { CommandBus, QueryBus } from '@nestjs/cqrs'; +import { CreateUserCommand } from '../../application/commands/create_user/create_user.command'; +import { GetUserRoleQuery } from '../../application/queries/get_user_role/get_user_role.query'; @Resolver() export class AuthResolver { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly commandBus: CommandBus, + private readonly queryBus: QueryBus, + ) {} @UseGuards(JwtAuthGuard) @Query(() => AuthUserVerification) async verify( @@ -25,7 +32,9 @@ export class AuthResolver { } const userInfo = await this.authService.getUserInfoFromAuth0(authorizationHeader); - const { id, role } = await this.authService.createUser(user.sub); + const { id, role } = await this.commandBus.execute( + new CreateUserCommand(user.sub), + ); await this.authService.addUserCreatedEvent(id, userInfo.nickname); return { authorized: true, @@ -36,7 +45,9 @@ export class AuthResolver { e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002' ) { - const userRole = await this.authService.getUserRole(user.sub); + const userRole = await this.queryBus.execute( + new GetUserRoleQuery(user.sub), + ); return { authorized: true, role: userRole?.role || null, diff --git a/apps/api/src/modules/auth/guards/admin-user.guard.ts b/apps/api/src/modules/auth/infrastructure/guards/admin-user.guard.ts similarity index 100% rename from apps/api/src/modules/auth/guards/admin-user.guard.ts rename to apps/api/src/modules/auth/infrastructure/guards/admin-user.guard.ts diff --git a/apps/api/src/modules/auth/guards/auth-jwt.guard.ts b/apps/api/src/modules/auth/infrastructure/guards/auth-jwt.guard.ts similarity index 72% rename from apps/api/src/modules/auth/guards/auth-jwt.guard.ts rename to apps/api/src/modules/auth/infrastructure/guards/auth-jwt.guard.ts index ea6ea88..6c05a88 100644 --- a/apps/api/src/modules/auth/guards/auth-jwt.guard.ts +++ b/apps/api/src/modules/auth/infrastructure/guards/auth-jwt.guard.ts @@ -1,11 +1,12 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { GqlExecutionContext } from '@nestjs/graphql'; -import { AuthService } from '../auth.service'; +import { QueryBus } from '@nestjs/cqrs'; +import { GetUserRoleQuery } from '../../application/queries/get_user_role/get_user_role.query'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { - constructor(private readonly authService: AuthService) { + constructor(private readonly queryBus: QueryBus) { super(); } @@ -16,7 +17,9 @@ export class JwtAuthGuard extends AuthGuard('jwt') { } const request = this.getRequest(context); const user = request.user; - const userWithRole = await this.authService.getUserRole(user.sub); + const userWithRole = await this.queryBus.execute( + new GetUserRoleQuery(user.sub), + ); if (userWithRole) { request.user = { ...user, diff --git a/apps/api/src/modules/auth/auth.service.ts b/apps/api/src/modules/auth/infrastructure/services/auth.service.ts similarity index 58% rename from apps/api/src/modules/auth/auth.service.ts rename to apps/api/src/modules/auth/infrastructure/services/auth.service.ts index 084ff15..d1ecc21 100644 --- a/apps/api/src/modules/auth/auth.service.ts +++ b/apps/api/src/modules/auth/infrastructure/services/auth.service.ts @@ -3,24 +3,15 @@ import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; -import { UserAuth0InfoDTO } from './auth.dto'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; -import { CreateUserCommand } from './commands/create_user/create_user.command'; -import { GetUserRoleQuery } from './queries/get_user_role/get_user_role.query'; +import { UserAuth0InfoDTO } from '../graphql/auth.dto'; @Injectable() export class AuthService { constructor( private readonly httpService: HttpService, - private readonly commandBus: CommandBus, - private readonly queryBus: QueryBus, @InjectQueue('user_created') private userCreatedQueue: Queue, ) {} - createUser(oauthId: string) { - return this.commandBus.execute(new CreateUserCommand(oauthId)); - } - async addUserCreatedEvent(userId: number, username: string) { await this.userCreatedQueue.add('userCreated', { userId, username }); } @@ -35,8 +26,4 @@ export class AuthService { ); return data; } - - async getUserRole(oauthId: string) { - return this.queryBus.execute(new GetUserRoleQuery(oauthId)); - } } diff --git a/apps/api/src/modules/auth/strategies/o-auth-jwt-strategy.service.ts b/apps/api/src/modules/auth/infrastructure/strategies/o-auth-jwt-strategy.service.ts similarity index 95% rename from apps/api/src/modules/auth/strategies/o-auth-jwt-strategy.service.ts rename to apps/api/src/modules/auth/infrastructure/strategies/o-auth-jwt-strategy.service.ts index b6dd218..8ced461 100644 --- a/apps/api/src/modules/auth/strategies/o-auth-jwt-strategy.service.ts +++ b/apps/api/src/modules/auth/infrastructure/strategies/o-auth-jwt-strategy.service.ts @@ -3,7 +3,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config'; import { passportJwtSecret } from 'jwks-rsa'; -import { UserAuthDTO } from '../auth.dto'; +import { UserAuthDTO } from '../graphql/auth.dto'; type OAuthPayload = { sub: string; diff --git a/apps/api/src/modules/collections/collection.resolver.ts b/apps/api/src/modules/collections/collection.resolver.ts index c6541d4..a37ff57 100644 --- a/apps/api/src/modules/collections/collection.resolver.ts +++ b/apps/api/src/modules/collections/collection.resolver.ts @@ -1,6 +1,6 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { HttpException, HttpStatus, UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; import { AddGameToCollectionDTO, CollectionDTO, @@ -10,8 +10,8 @@ import { RemoveCollectionArgsDTO, RemovedCollectionResponseDTO, } from './collections.dto'; -import { UserAuthDTO } from '../auth/auth.dto'; -import { User } from '../auth/auth.decorators'; +import { UserAuthDTO } from '../auth/infrastructure/graphql/auth.dto'; +import { User } from '../auth/infrastructure/decorators/auth.decorators'; import { ProfilesService } from '../profiles/profiles.service'; import { CollectionsService } from './collections.service'; diff --git a/apps/api/src/modules/friends/friends_activity/friends_activity.resolver.ts b/apps/api/src/modules/friends/friends_activity/friends_activity.resolver.ts index 51eeb2e..c036031 100644 --- a/apps/api/src/modules/friends/friends_activity/friends_activity.resolver.ts +++ b/apps/api/src/modules/friends/friends_activity/friends_activity.resolver.ts @@ -1,8 +1,8 @@ import { Injectable, NotFoundException, UseGuards } from '@nestjs/common'; import { FriendsActivityService } from './friends_activity.service'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; -import { User } from '../../auth/auth.decorators'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; import { FriendsActivityDTO } from './friends_activity.dto'; import { Query } from '@nestjs/graphql'; diff --git a/apps/api/src/modules/friends/friends_list/friends_list.resolver.ts b/apps/api/src/modules/friends/friends_list/friends_list.resolver.ts index 7e07e5e..ad6dc33 100644 --- a/apps/api/src/modules/friends/friends_list/friends_list.resolver.ts +++ b/apps/api/src/modules/friends/friends_list/friends_list.resolver.ts @@ -1,9 +1,9 @@ import { Injectable, UseGuards } from '@nestjs/common'; import { Query } from '@nestjs/graphql'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; import { FriendsListService } from './friends_list.service'; -import { User } from '../../auth/auth.decorators'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; import { FriendsListDTO } from './friends_list.dto'; @Injectable() diff --git a/apps/api/src/modules/friends/friends_requests/friends_requests.resolver.ts b/apps/api/src/modules/friends/friends_requests/friends_requests.resolver.ts index 9725f04..9d71a36 100644 --- a/apps/api/src/modules/friends/friends_requests/friends_requests.resolver.ts +++ b/apps/api/src/modules/friends/friends_requests/friends_requests.resolver.ts @@ -1,13 +1,13 @@ import { Injectable, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query } from '@nestjs/graphql'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; import { FriendRequestResponseDTO, GetFriendRequestsResponseDTO, } from './friends_requests.dto'; -import { User } from '../../auth/auth.decorators'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; import { FriendsRequestsService } from './friends_requests.service'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; @Injectable() export class FriendsRequestsResolver { diff --git a/apps/api/src/modules/friends/friends_search/friends_search.resolver.ts b/apps/api/src/modules/friends/friends_search/friends_search.resolver.ts index a9d9a36..14c20a7 100644 --- a/apps/api/src/modules/friends/friends_search/friends_search.resolver.ts +++ b/apps/api/src/modules/friends/friends_search/friends_search.resolver.ts @@ -1,8 +1,8 @@ import { Injectable, UseGuards } from '@nestjs/common'; import { Args, Query } from '@nestjs/graphql'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; -import { User } from '../../auth/auth.decorators'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; import { UserSearchResultDTO } from './friends_resolver.dto'; import { FriendsSearchService } from './friends_search.service'; diff --git a/apps/api/src/modules/games/games.resolver.ts b/apps/api/src/modules/games/games.resolver.ts index 44fbc79..a973089 100644 --- a/apps/api/src/modules/games/games.resolver.ts +++ b/apps/api/src/modules/games/games.resolver.ts @@ -8,8 +8,8 @@ import { UpdateGameDataDTO, } from './games.dto'; import { UseGuards } from '@nestjs/common'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; import { QueryBus } from '@nestjs/cqrs'; import { GetUpcomingGamesQuery } from './queries/get_upcoming_games/get_upcoming_games.query'; diff --git a/apps/api/src/modules/games_status/games_status.resolver.ts b/apps/api/src/modules/games_status/games_status.resolver.ts index bcc0b95..a3c55e1 100644 --- a/apps/api/src/modules/games_status/games_status.resolver.ts +++ b/apps/api/src/modules/games_status/games_status.resolver.ts @@ -13,16 +13,16 @@ import { } from './games_status.dto'; import { GamesStatusService } from './games_status.service'; import { HttpException, HttpStatus, UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { User } from '../auth/auth.decorators'; -import { UserAuthDTO } from '../auth/auth.dto'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { User } from '../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../auth/infrastructure/graphql/auth.dto'; import { GetAllUserFriendGamesStatusArgs, GetAllUserGamesStatusArgs, } from './games_status.args'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; import { QueryBus } from '@nestjs/cqrs'; import { GetLastEditedGamesQuery } from './queries/get_last_edited_games/get_last_edited_games.query'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; @Resolver() export class GamesStatusResolver { diff --git a/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration.controller.ts b/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration.controller.ts index 90c3cf5..bbbea82 100644 --- a/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration.controller.ts +++ b/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration.controller.ts @@ -12,8 +12,8 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { ParseCsvFilePipe } from './howlongtobeat_migration.pipe'; import { HowLongToBeatMigrationService } from './howlongtobeat_migration.service'; import { HowLongToBeatAccountCsvGamesSchema } from './howlongtobeat_migration.dto'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { RequestWithUser } from '../auth/auth.dto'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { RequestWithUser } from '../auth/infrastructure/graphql/auth.dto'; import { HowLongToBeatMigrationStatusService } from './howlongtobeat_migration_status/howlongtobeat_migration_status.service'; import { MigrationStatus } from '@prisma/client'; diff --git a/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration_status/howlongtobeat_migration_status.resolver.ts b/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration_status/howlongtobeat_migration_status.resolver.ts index c9b5d1d..9324f26 100644 --- a/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration_status/howlongtobeat_migration_status.resolver.ts +++ b/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration_status/howlongtobeat_migration_status.resolver.ts @@ -1,9 +1,9 @@ import { Query, Resolver } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; import { HowLongToBeatMigrationStatusService } from './howlongtobeat_migration_status.service'; -import { User } from '../../auth/auth.decorators'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; import { HowLongToBeatMigrationStatusDTO } from './howlongtobeat_migration_status.dto'; @Resolver() diff --git a/apps/api/src/modules/images/images.controller.ts b/apps/api/src/modules/images/images.controller.ts index a6fa31c..3e35647 100644 --- a/apps/api/src/modules/images/images.controller.ts +++ b/apps/api/src/modules/images/images.controller.ts @@ -9,7 +9,7 @@ import { } from '@nestjs/common'; import { ImagesService } from './images.service'; import { FileInterceptor } from '@nestjs/platform-express'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; import { ZodValidationPipe } from '../validation/validation.pipe'; import { ImageUploadDTO, diff --git a/apps/api/src/modules/platforms/platforms.resolver.ts b/apps/api/src/modules/platforms/platforms.resolver.ts index 4254578..fa803e9 100644 --- a/apps/api/src/modules/platforms/platforms.resolver.ts +++ b/apps/api/src/modules/platforms/platforms.resolver.ts @@ -1,9 +1,9 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; import { PlatformsService } from './platforms.service'; import { PlatformsDTO, UpdatePlatformDisplayNameDTO } from './platforms.dto'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; @Resolver() export class PlatformsResolver { diff --git a/apps/api/src/modules/profiles/profiles.resolver.ts b/apps/api/src/modules/profiles/profiles.resolver.ts index bfb312d..8f1cfa7 100644 --- a/apps/api/src/modules/profiles/profiles.resolver.ts +++ b/apps/api/src/modules/profiles/profiles.resolver.ts @@ -5,9 +5,9 @@ import { ProfileInfoUpdateResponseDTO, } from './profiles.dto'; import { UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { User } from '../auth/auth.decorators'; -import { UserAuthDTO } from '../auth/auth.dto'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { UserAuthDTO } from '../auth/infrastructure/graphql/auth.dto'; +import { User } from '../auth/infrastructure/decorators/auth.decorators'; import { ProfilesRepository } from './profiles.repository'; @Resolver() diff --git a/apps/api/src/modules/roles/roles.resolver.ts b/apps/api/src/modules/roles/roles.resolver.ts index 9c87d7c..bee0c08 100644 --- a/apps/api/src/modules/roles/roles.resolver.ts +++ b/apps/api/src/modules/roles/roles.resolver.ts @@ -2,8 +2,8 @@ import { Injectable, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query } from '@nestjs/graphql'; import { RoleDTO, UpdateUserRoleDTO, UpdateUserRoleInput } from './roles.dto'; import { RolesService } from './roles.service'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; @Injectable() export class RolesResolver { diff --git a/apps/api/src/modules/user_stats/user_stats.resolver.ts b/apps/api/src/modules/user_stats/user_stats.resolver.ts index 9dd204e..1097af5 100644 --- a/apps/api/src/modules/user_stats/user_stats.resolver.ts +++ b/apps/api/src/modules/user_stats/user_stats.resolver.ts @@ -2,9 +2,9 @@ import { Resolver, Query, Args } from '@nestjs/graphql'; import { UserStatsService } from './user_stats.service'; import { GetUserStatsArgs, UserStatsDTO } from './user_stats.dto'; import { UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { User } from '../auth/auth.decorators'; -import { UserAuthDTO } from '../auth/auth.dto'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { User } from '../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../auth/infrastructure/graphql/auth.dto'; @Resolver() export class UserStatsResolver { diff --git a/apps/api/src/modules/users/users.resolver.ts b/apps/api/src/modules/users/users.resolver.ts index 6f819ff..173ee43 100644 --- a/apps/api/src/modules/users/users.resolver.ts +++ b/apps/api/src/modules/users/users.resolver.ts @@ -2,8 +2,8 @@ import { Injectable, NotFoundException, UseGuards } from '@nestjs/common'; import { UserDataDTO, UserDTO } from './users.dto'; import { Args, Query } from '@nestjs/graphql'; import { UsersService } from './users.service'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; @Injectable() export class UsersResolver { diff --git a/apps/native/modules/screens/homepage/home_screen.tsx b/apps/native/modules/screens/homepage/home_screen.tsx index d264a08..c61c5d4 100644 --- a/apps/native/modules/screens/homepage/home_screen.tsx +++ b/apps/native/modules/screens/homepage/home_screen.tsx @@ -40,10 +40,10 @@ export const HomeScreen = () => { + + + - - -