From 67830202c54789be6ab4196b5e85ffd918834584 Mon Sep 17 00:00:00 2001 From: dangreen Date: Mon, 4 May 2026 02:38:45 +0400 Subject: [PATCH] docs: unify and simplify examples --- .gitignore | 2 +- examples/event-board/common/api/events.js | 262 +++ examples/event-board/common/api/index.js | 71 + examples/event-board/common/api/server.js | 20 + examples/event-board/common/eslint.config.js | 28 + examples/event-board/common/package.json | 19 + examples/event-board/common/src/routes.ts | 7 + .../event-board/common/src/services/events.ts | 107 + .../common/src/services/events.types.ts | 39 + .../common/src/stores-di/events.ts | 256 +++ .../event-board/common/src/stores-di/query.ts | 16 + .../common/src/stores-di/router.ts | 36 + .../event-board/common/src/stores-di/utils.ts | 3 + examples/event-board/common/tsconfig.json | 28 + .../rick-and-morty/common/eslint.config.js | 22 + examples/rick-and-morty/common/package.json | 20 + .../common/src/common/constants.ts | 2 + examples/rick-and-morty/common/src/routes.ts | 7 + .../rick-and-morty/common/src/services/api.ts | 78 + .../common/src/services/types.ts | 63 + .../common/src/stores-di/characters.ts | 127 ++ .../common/src/stores-di/episodes.ts | 118 ++ .../common/src/stores-di/locations.ts | 85 + .../common/src/stores-di/memoryStorage.ts | 82 + .../common/src/stores-di/query.ts | 19 + .../common/src/stores-di/router.ts | 40 + .../common/src/stores/characters.ts | 98 + .../common/src/stores/episodes.ts | 91 + .../common/src/stores/locations.ts | 67 + .../rick-and-morty/common/src/stores/query.ts | 15 + .../common/src/stores/router.ts | 34 + examples/rick-and-morty/common/tsconfig.json | 28 + .../src/app/character/[characterId]/page.tsx | 3 +- .../src/app/characters/page.tsx | 3 +- .../src/app/episode/[episodeId]/page.tsx | 3 +- .../src/app/episodes/page.tsx | 3 +- .../next-app-nano_kit-ssr/src/app/globals.css | 1570 +++++++++++++++ .../src/app/layout.module.css | 11 - .../next-app-nano_kit-ssr/src/app/layout.tsx | 5 +- .../src/app/location/[locationId]/page.tsx | 3 +- .../src/app/locations/page.tsx | 3 +- .../src/common/constants.ts | 1 + .../next-app-nano_kit-ssr/src/services/api.ts | 108 +- .../src/services/types.ts | 10 - .../src/stores/characters.ts | 53 +- .../src/stores/episodes.ts | 40 +- .../src/stores/locations.ts | 4 +- .../next-app-nano_kit-ssr/src/stores/query.ts | 6 +- .../src/ui/blocks/CharacterCard.tsx | 70 + .../CharacterCard/CharacterCard.module.css | 166 -- .../ui/blocks/CharacterCard/CharacterCard.tsx | 71 - .../src/ui/blocks/CharacterCard/index.ts | 1 - .../src/ui/blocks}/CharacterDetail.tsx | 27 +- .../CharacterDetail.module.css | 154 -- .../src/ui/blocks/CharacterDetail/index.ts | 1 - .../{CharactersGrid => }/CharactersGrid.tsx | 3 +- .../CharactersGrid/CharactersGrid.module.css | 22 - .../src/ui/blocks/CharactersGrid/index.ts | 1 - .../src/ui/blocks/EpisodeCard.tsx | 43 + .../blocks/EpisodeCard/EpisodeCard.module.css | 108 - .../src/ui/blocks/EpisodeCard/EpisodeCard.tsx | 44 - .../src/ui/blocks/EpisodeCard/index.ts | 1 - .../src/ui/blocks/EpisodeDetail.tsx | 46 + .../EpisodeDetail/EpisodeDetail.module.css | 127 -- .../ui/blocks/EpisodeDetail/EpisodeDetail.tsx | 47 - .../src/ui/blocks/EpisodeDetail/index.ts | 1 - .../src/ui/blocks}/EpisodesGrid.tsx | 3 +- .../EpisodesGrid/EpisodesGrid.module.css | 22 - .../src/ui/blocks/EpisodesGrid/index.ts | 1 - .../src/ui/blocks/{Header => }/Header.tsx | 17 +- .../src/ui/blocks/Header/Header.module.css | 110 - .../src/ui/blocks/Header/index.ts | 1 - .../src/ui/blocks/LocationCard.tsx | 43 + .../LocationCard/LocationCard.module.css | 108 - .../ui/blocks/LocationCard/LocationCard.tsx | 44 - .../src/ui/blocks/LocationCard/index.ts | 1 - .../src/ui/blocks/LocationDetail.tsx | 46 + .../LocationDetail/LocationDetail.module.css | 127 -- .../blocks/LocationDetail/LocationDetail.tsx | 47 - .../src/ui/blocks/LocationDetail/index.ts | 1 - .../{LocationsGrid => }/LocationsGrid.tsx | 3 +- .../LocationsGrid/LocationsGrid.module.css | 22 - .../src/ui/blocks/LocationsGrid/index.ts | 1 - .../{Pagination => }/Pagination.tsx | 19 +- .../Pagination/Pagination.module.css | 143 -- .../src/ui/components/Pagination/index.ts | 1 - .../ui/components/{Spinner => }/Spinner.tsx | 5 +- .../ui/components/Spinner/Spinner.module.css | 23 - .../src/ui/components/Spinner/index.ts | 1 - .../stores.ts => Character.stores.ts} | 0 .../ui/pages/{Character => }/Character.tsx | 9 +- .../ui/pages/Character/Character.module.css | 73 - .../src/ui/pages/Character/index.ts | 3 - .../stores.ts => Characters.stores.ts} | 0 .../ui/pages/{Characters => }/Characters.tsx | 9 +- .../ui/pages/Characters/Characters.module.css | 49 - .../src/ui/pages/Characters/index.ts | 3 - .../{Episode/stores.ts => Episode.stores.ts} | 0 .../src/ui/pages/{Episode => }/Episode.tsx | 9 +- .../src/ui/pages/Episode/Episode.module.css | 74 - .../src/ui/pages/Episode/index.ts | 3 - .../stores.ts => Episodes.stores.ts} | 0 .../src/ui/pages}/Episodes.tsx | 9 +- .../src/ui/pages/Episodes/Episodes.module.css | 28 - .../src/ui/pages/Episodes/index.ts | 3 - .../stores.ts => Location.stores.ts} | 0 .../src/ui/pages/{Location => }/Location.tsx | 9 +- .../src/ui/pages/Location/Location.module.css | 73 - .../src/ui/pages/Location/index.ts | 3 - .../stores.ts => Locations.stores.ts} | 0 .../ui/pages/{Locations => }/Locations.tsx | 9 +- .../ui/pages/Locations/Locations.module.css | 71 - .../src/ui/pages/Locations/index.ts | 3 - .../src/common/constants.ts | 1 + .../src/pages/_app.tsx | 5 +- .../src/pages/character/[characterId].tsx | 3 +- .../src/pages/characters.tsx | 3 +- .../src/pages/episode/[episodeId].tsx | 3 +- .../src/pages/episodes.tsx | 3 +- .../src/pages/location/[locationId].tsx | 3 +- .../src/pages/locations.tsx | 3 +- .../src/services/api.ts | 108 +- .../src/services/types.ts | 10 - .../src/stores/characters.ts | 53 +- .../src/stores/episodes.ts | 40 +- .../src/stores/locations.ts | 4 +- .../src/stores/query.ts | 6 +- .../src/styles/globals.css | 1570 +++++++++++++++ .../src/styles/layout.module.css | 11 - .../src/ui/blocks/CharacterCard.tsx | 70 + .../CharacterCard/CharacterCard.module.css | 166 -- .../ui/blocks/CharacterCard/CharacterCard.tsx | 71 - .../src/ui/blocks/CharacterCard/index.ts | 1 - .../src/ui/blocks}/CharacterDetail.tsx | 27 +- .../CharacterDetail.module.css | 154 -- .../src/ui/blocks/CharacterDetail/index.ts | 1 - .../{CharactersGrid => }/CharactersGrid.tsx | 3 +- .../CharactersGrid/CharactersGrid.module.css | 22 - .../src/ui/blocks/CharactersGrid/index.ts | 1 - .../src/ui/blocks/EpisodeCard.tsx | 45 + .../blocks/EpisodeCard/EpisodeCard.module.css | 108 - .../src/ui/blocks/EpisodeCard/EpisodeCard.tsx | 46 - .../src/ui/blocks/EpisodeCard/index.ts | 1 - .../src/ui/blocks/EpisodeDetail.tsx | 46 + .../EpisodeDetail/EpisodeDetail.module.css | 127 -- .../ui/blocks/EpisodeDetail/EpisodeDetail.tsx | 47 - .../src/ui/blocks/EpisodeDetail/index.ts | 1 - .../src/ui/blocks}/EpisodesGrid.tsx | 3 +- .../EpisodesGrid/EpisodesGrid.module.css | 22 - .../src/ui/blocks/EpisodesGrid/index.ts | 1 - .../src/ui/blocks/{Header => }/Header.tsx | 17 +- .../src/ui/blocks/Header/Header.module.css | 110 - .../src/ui/blocks/Header/index.ts | 1 - .../src/ui/blocks/LocationCard.tsx | 43 + .../LocationCard/LocationCard.module.css | 108 - .../ui/blocks/LocationCard/LocationCard.tsx | 44 - .../src/ui/blocks/LocationCard/index.ts | 1 - .../src/ui/blocks/LocationDetail.tsx | 46 + .../LocationDetail/LocationDetail.module.css | 127 -- .../blocks/LocationDetail/LocationDetail.tsx | 47 - .../src/ui/blocks/LocationDetail/index.ts | 1 - .../{LocationsGrid => }/LocationsGrid.tsx | 3 +- .../LocationsGrid/LocationsGrid.module.css | 22 - .../src/ui/blocks/LocationsGrid/index.ts | 1 - .../{Pagination => }/Pagination.tsx | 19 +- .../Pagination/Pagination.module.css | 143 -- .../src/ui/components/Pagination/index.ts | 1 - .../ui/components/{Spinner => }/Spinner.tsx | 5 +- .../ui/components/Spinner/Spinner.module.css | 23 - .../src/ui/components/Spinner/index.ts | 1 - .../stores.ts => Character.stores.ts} | 0 .../ui/pages/{Character => }/Character.tsx | 9 +- .../ui/pages/Character/Character.module.css | 73 - .../src/ui/pages/Character/index.ts | 3 - .../stores.ts => Characters.stores.ts} | 0 .../ui/pages/{Characters => }/Characters.tsx | 9 +- .../ui/pages/Characters/Characters.module.css | 49 - .../src/ui/pages/Characters/index.ts | 3 - .../{Episode/stores.ts => Episode.stores.ts} | 0 .../src/ui/pages/{Episode => }/Episode.tsx | 9 +- .../src/ui/pages/Episode/Episode.module.css | 74 - .../src/ui/pages/Episode/index.ts | 3 - .../stores.ts => Episodes.stores.ts} | 0 .../src/ui/pages}/Episodes.tsx | 9 +- .../src/ui/pages/Episodes/Episodes.module.css | 28 - .../src/ui/pages/Episodes/index.ts | 3 - .../stores.ts => Location.stores.ts} | 0 .../src/ui/pages/{Location => }/Location.tsx | 9 +- .../src/ui/pages/Location/Location.module.css | 73 - .../src/ui/pages/Location/index.ts | 3 - .../stores.ts => Locations.stores.ts} | 0 .../ui/pages/{Locations => }/Locations.tsx | 9 +- .../ui/pages/Locations/Locations.module.css | 71 - .../src/ui/pages/Locations/index.ts | 3 - .../react-nano_kit-ssr/src/app.css | 1766 +++++++++++++++++ .../src/common/constants.ts | 1 + .../react-nano_kit-ssr/src/services/api.ts | 108 +- .../react-nano_kit-ssr/src/services/types.ts | 10 - .../src/stores/characters.ts | 53 +- .../react-nano_kit-ssr/src/stores/episodes.ts | 40 +- .../src/stores/locations.ts | 4 +- .../react-nano_kit-ssr/src/stores/query.ts | 6 +- .../src/ui/blocks/CharacterCard.tsx | 69 + .../CharacterCard/CharacterCard.module.css | 167 -- .../ui/blocks/CharacterCard/CharacterCard.tsx | 70 - .../src/ui/blocks/CharacterCard/index.ts | 1 - .../{CharacterDetail => }/CharacterDetail.tsx | 27 +- .../CharacterDetail.module.css | 154 -- .../src/ui/blocks/CharacterDetail/index.ts | 1 - .../{CharactersGrid => }/CharactersGrid.tsx | 3 +- .../CharactersGrid/CharactersGrid.module.css | 22 - .../src/ui/blocks/CharactersGrid/index.ts | 1 - .../src/ui/blocks/EpisodeCard.tsx | 42 + .../blocks/EpisodeCard/EpisodeCard.module.css | 108 - .../src/ui/blocks/EpisodeCard/EpisodeCard.tsx | 43 - .../src/ui/blocks/EpisodeCard/index.ts | 1 - .../src/ui/blocks/EpisodeDetail.tsx | 45 + .../EpisodeDetail/EpisodeDetail.module.css | 127 -- .../ui/blocks/EpisodeDetail/EpisodeDetail.tsx | 46 - .../src/ui/blocks/EpisodeDetail/index.ts | 1 - .../{EpisodesGrid => }/EpisodesGrid.tsx | 3 +- .../EpisodesGrid/EpisodesGrid.module.css | 22 - .../src/ui/blocks/EpisodesGrid/index.ts | 1 - .../src/ui/blocks/LocationCard.tsx | 42 + .../LocationCard/LocationCard.module.css | 108 - .../ui/blocks/LocationCard/LocationCard.tsx | 43 - .../src/ui/blocks/LocationCard/index.ts | 1 - .../src/ui/blocks/LocationDetail.tsx | 45 + .../LocationDetail/LocationDetail.module.css | 127 -- .../blocks/LocationDetail/LocationDetail.tsx | 46 - .../src/ui/blocks/LocationDetail/index.ts | 1 - .../src/ui/blocks}/LocationsGrid.tsx | 3 +- .../LocationsGrid/LocationsGrid.module.css | 22 - .../src/ui/blocks/LocationsGrid/index.ts | 1 - .../{Pagination => }/Pagination.tsx | 19 +- .../Pagination/Pagination.module.css | 143 -- .../src/ui/components/Pagination/index.ts | 1 - .../ui/components/{Spinner => }/Spinner.tsx | 5 +- .../ui/components/Spinner/Spinner.module.css | 23 - .../src/ui/components/Spinner/index.ts | 1 - .../ui/pages/{Character => }/Character.tsx | 9 +- .../ui/pages/Character/Character.module.css | 134 -- .../src/ui/pages/Character/index.ts | 2 - .../ui/pages/{Characters => }/Characters.tsx | 9 +- .../ui/pages/Characters/Characters.module.css | 72 - .../src/ui/pages/Characters/index.ts | 2 - .../src/ui/pages/{Episode => }/Episode.tsx | 9 +- .../src/ui/pages/Episode/Episode.module.css | 74 - .../src/ui/pages/Episode/index.ts | 2 - .../src/ui/pages/{Episodes => }/Episodes.tsx | 9 +- .../src/ui/pages/Episodes/Episodes.module.css | 72 - .../src/ui/pages/Episodes/index.ts | 2 - .../src/ui/pages/{Home => }/Home.tsx | 2 +- .../src/ui/pages/Home/index.ts | 2 - .../src/ui/pages/{Location => }/Location.tsx | 9 +- .../src/ui/pages/Location/Location.module.css | 130 -- .../src/ui/pages/Location/index.ts | 2 - .../ui/pages/{Locations => }/Locations.tsx | 9 +- .../ui/pages/Locations/Locations.module.css | 72 - .../src/ui/pages/Locations/index.ts | 2 - .../ui/pages/{MainLayout => }/MainLayout.tsx | 21 +- .../ui/pages/MainLayout/MainLayout.module.css | 133 -- .../src/ui/pages/MainLayout/index.ts | 2 - .../rick-and-morty/react-nano_kit/src/app.css | 1766 +++++++++++++++++ .../react-nano_kit/src/common/constants.ts | 1 + .../react-nano_kit/src/services/api.ts | 108 +- .../react-nano_kit/src/services/types.ts | 10 - .../react-nano_kit/src/stores/characters.ts | 48 +- .../react-nano_kit/src/stores/episodes.ts | 36 +- .../react-nano_kit/src/stores/locations.ts | 4 +- .../src/ui/blocks/CharacterCard.tsx | 68 + .../CharacterCard/CharacterCard.module.css | 167 -- .../ui/blocks/CharacterCard/CharacterCard.tsx | 69 - .../src/ui/blocks/CharacterCard/index.ts | 1 - .../src/ui/blocks/CharacterDetail.tsx | 51 + .../CharacterDetail.module.css | 154 -- .../CharacterDetail/CharacterDetail.tsx | 52 - .../src/ui/blocks/CharacterDetail/index.ts | 1 - .../src/ui/blocks}/CharactersGrid.tsx | 3 +- .../CharactersGrid/CharactersGrid.module.css | 22 - .../src/ui/blocks/CharactersGrid/index.ts | 1 - .../src/ui/blocks/EpisodeCard.tsx | 42 + .../blocks/EpisodeCard/EpisodeCard.module.css | 108 - .../src/ui/blocks/EpisodeCard/EpisodeCard.tsx | 43 - .../src/ui/blocks/EpisodeCard/index.ts | 1 - .../src/ui/blocks/EpisodeDetail.tsx | 41 + .../EpisodeDetail/EpisodeDetail.module.css | 127 -- .../ui/blocks/EpisodeDetail/EpisodeDetail.tsx | 42 - .../src/ui/blocks/EpisodeDetail/index.ts | 1 - .../src/ui/blocks}/EpisodesGrid.tsx | 3 +- .../EpisodesGrid/EpisodesGrid.module.css | 22 - .../src/ui/blocks/EpisodesGrid/index.ts | 1 - .../src/ui/blocks/LocationCard.tsx | 42 + .../LocationCard/LocationCard.module.css | 108 - .../ui/blocks/LocationCard/LocationCard.tsx | 43 - .../src/ui/blocks/LocationCard/index.ts | 1 - .../src/ui/blocks/LocationDetail.tsx | 41 + .../LocationDetail/LocationDetail.module.css | 127 -- .../blocks/LocationDetail/LocationDetail.tsx | 42 - .../src/ui/blocks/LocationDetail/index.ts | 1 - .../{LocationsGrid => }/LocationsGrid.tsx | 3 +- .../LocationsGrid/LocationsGrid.module.css | 22 - .../src/ui/blocks/LocationsGrid/index.ts | 1 - .../src/ui/components/{Link => }/Link.tsx | 0 .../src/ui/components/Link/index.ts | 1 - .../{Pagination => }/Pagination.tsx | 19 +- .../Pagination/Pagination.module.css | 143 -- .../src/ui/components/Pagination/index.ts | 1 - .../src/ui/components}/Spinner.tsx | 5 +- .../ui/components/Spinner/Spinner.module.css | 23 - .../src/ui/components/Spinner/index.ts | 1 - .../ui/pages/{Character => }/Character.tsx | 11 +- .../ui/pages/Character/Character.module.css | 134 -- .../src/ui/pages/Character/index.ts | 2 - .../ui/pages/{Characters => }/Characters.tsx | 11 +- .../ui/pages/Characters/Characters.module.css | 72 - .../src/ui/pages/Characters/index.ts | 2 - .../src/ui/pages/{Episode => }/Episode.tsx | 11 +- .../src/ui/pages/Episode/Episode.module.css | 74 - .../src/ui/pages/Episode/index.ts | 2 - .../src/ui/pages/{Episodes => }/Episodes.tsx | 11 +- .../src/ui/pages/Episodes/Episodes.module.css | 72 - .../src/ui/pages/Episodes/index.ts | 2 - .../src/ui/pages/{Home => }/Home.tsx | 2 +- .../react-nano_kit/src/ui/pages/Home/index.ts | 1 - .../src/ui/pages/{Location => }/Location.tsx | 11 +- .../src/ui/pages/Location/Location.module.css | 130 -- .../src/ui/pages/Location/index.ts | 2 - .../ui/pages/{Locations => }/Locations.tsx | 11 +- .../ui/pages/Locations/Locations.module.css | 72 - .../src/ui/pages/Locations/index.ts | 2 - .../src/ui/pages/MainLayout.tsx | 33 + .../ui/pages/MainLayout/MainLayout.module.css | 133 -- .../src/ui/pages/MainLayout/MainLayout.tsx | 34 - .../src/ui/pages/MainLayout/index.ts | 1 - .../react-tanstack-start-ssr/src/app.css | 1766 +++++++++++++++++ .../src/routes/character.$characterId.tsx | 7 +- .../src/routes/episode.$episodeId.tsx | 7 +- .../src/routes/location.$locationId.tsx | 7 +- .../src/services/api.ts | 169 +- .../src/services/types.ts | 10 - .../src/stores/characters.ts | 73 +- .../src/stores/episodes.ts | 52 +- .../src/stores/locations.ts | 31 +- .../src/ui/blocks/CharacterCard.tsx | 65 + .../CharacterCard/CharacterCard.module.css | 167 -- .../ui/blocks/CharacterCard/CharacterCard.tsx | 66 - .../src/ui/blocks/CharacterCard/index.ts | 1 - .../src/ui/blocks/CharacterDetail.tsx | 48 + .../CharacterDetail.module.css | 154 -- .../CharacterDetail/CharacterDetail.tsx | 49 - .../src/ui/blocks/CharacterDetail/index.ts | 1 - .../src/ui/blocks}/CharactersGrid.tsx | 3 +- .../CharactersGrid/CharactersGrid.module.css | 22 - .../src/ui/blocks/CharactersGrid/index.ts | 1 - .../src/ui/blocks/EpisodeCard.tsx | 38 + .../blocks/EpisodeCard/EpisodeCard.module.css | 108 - .../src/ui/blocks/EpisodeCard/EpisodeCard.tsx | 39 - .../src/ui/blocks/EpisodeCard/index.ts | 1 - .../src/ui/blocks/EpisodeDetail.tsx | 38 + .../EpisodeDetail/EpisodeDetail.module.css | 127 -- .../ui/blocks/EpisodeDetail/EpisodeDetail.tsx | 39 - .../src/ui/blocks/EpisodeDetail/index.ts | 1 - .../src/ui/blocks}/EpisodesGrid.tsx | 3 +- .../EpisodesGrid/EpisodesGrid.module.css | 22 - .../src/ui/blocks/EpisodesGrid/index.ts | 1 - .../src/ui/blocks/LocationCard.tsx | 38 + .../LocationCard/LocationCard.module.css | 108 - .../ui/blocks/LocationCard/LocationCard.tsx | 39 - .../src/ui/blocks/LocationCard/index.ts | 1 - .../src/ui/blocks/LocationDetail.tsx | 38 + .../LocationDetail/LocationDetail.module.css | 127 -- .../blocks/LocationDetail/LocationDetail.tsx | 39 - .../src/ui/blocks/LocationDetail/index.ts | 1 - .../src/ui/blocks}/LocationsGrid.tsx | 3 +- .../LocationsGrid/LocationsGrid.module.css | 22 - .../src/ui/blocks/LocationsGrid/index.ts | 1 - .../src/ui/components}/Pagination.tsx | 19 +- .../Pagination/Pagination.module.css | 143 -- .../src/ui/components/Pagination/index.ts | 1 - .../src/ui/components}/Spinner.tsx | 5 +- .../ui/components/Spinner/Spinner.module.css | 23 - .../src/ui/components/Spinner/index.ts | 1 - .../ui/pages/{Character => }/Character.tsx | 15 +- .../ui/pages/Character/Character.module.css | 134 -- .../src/ui/pages/Character/index.ts | 2 - .../ui/pages/{Characters => }/Characters.tsx | 9 +- .../ui/pages/Characters/Characters.module.css | 72 - .../src/ui/pages/Characters/index.ts | 2 - .../src/ui/pages/{Episode => }/Episode.tsx | 15 +- .../src/ui/pages/Episode/Episode.module.css | 74 - .../src/ui/pages/Episode/index.ts | 2 - .../src/ui/pages}/Episodes.tsx | 9 +- .../src/ui/pages/Episodes/Episodes.module.css | 72 - .../src/ui/pages/Episodes/index.ts | 2 - .../src/ui/pages/{Location => }/Location.tsx | 15 +- .../src/ui/pages/Location/Location.module.css | 130 -- .../src/ui/pages/Location/index.ts | 2 - .../ui/pages/{Locations => }/Locations.tsx | 9 +- .../ui/pages/Locations/Locations.module.css | 72 - .../src/ui/pages/Locations/index.ts | 2 - .../ui/pages/{MainLayout => }/MainLayout.tsx | 21 +- .../ui/pages/MainLayout/MainLayout.module.css | 133 -- .../src/ui/pages/MainLayout/index.ts | 1 - .../rick-and-morty/react-tanstack/src/app.css | 1766 +++++++++++++++++ .../react-tanstack/src/routes/character.tsx | 7 +- .../react-tanstack/src/routes/episode.tsx | 7 +- .../react-tanstack/src/routes/location.tsx | 7 +- .../react-tanstack/src/services/api.ts | 169 +- .../react-tanstack/src/services/types.ts | 10 - .../react-tanstack/src/stores/characters.ts | 73 +- .../react-tanstack/src/stores/episodes.ts | 52 +- .../react-tanstack/src/stores/locations.ts | 31 +- .../src/ui/blocks/CharacterCard.tsx | 65 + .../CharacterCard/CharacterCard.module.css | 167 -- .../ui/blocks/CharacterCard/CharacterCard.tsx | 66 - .../src/ui/blocks/CharacterCard/index.ts | 1 - .../src/ui/blocks/CharacterDetail.tsx | 48 + .../CharacterDetail.module.css | 154 -- .../CharacterDetail/CharacterDetail.tsx | 49 - .../src/ui/blocks/CharacterDetail/index.ts | 1 - .../src/ui/blocks}/CharactersGrid.tsx | 3 +- .../CharactersGrid/CharactersGrid.module.css | 22 - .../src/ui/blocks/CharactersGrid/index.ts | 1 - .../src/ui/blocks/EpisodeCard.tsx | 38 + .../blocks/EpisodeCard/EpisodeCard.module.css | 108 - .../src/ui/blocks/EpisodeCard/EpisodeCard.tsx | 39 - .../src/ui/blocks/EpisodeCard/index.ts | 1 - .../src/ui/blocks/EpisodeDetail.tsx | 38 + .../EpisodeDetail/EpisodeDetail.module.css | 127 -- .../ui/blocks/EpisodeDetail/EpisodeDetail.tsx | 39 - .../src/ui/blocks/EpisodeDetail/index.ts | 1 - .../{EpisodesGrid => }/EpisodesGrid.tsx | 3 +- .../EpisodesGrid/EpisodesGrid.module.css | 22 - .../src/ui/blocks/EpisodesGrid/index.ts | 1 - .../src/ui/blocks/LocationCard.tsx | 38 + .../LocationCard/LocationCard.module.css | 108 - .../ui/blocks/LocationCard/LocationCard.tsx | 39 - .../src/ui/blocks/LocationCard/index.ts | 1 - .../src/ui/blocks/LocationDetail.tsx | 38 + .../LocationDetail/LocationDetail.module.css | 127 -- .../blocks/LocationDetail/LocationDetail.tsx | 39 - .../src/ui/blocks/LocationDetail/index.ts | 1 - .../src/ui/blocks}/LocationsGrid.tsx | 3 +- .../LocationsGrid/LocationsGrid.module.css | 22 - .../src/ui/blocks/LocationsGrid/index.ts | 1 - .../src/ui/components}/Pagination.tsx | 19 +- .../Pagination/Pagination.module.css | 143 -- .../src/ui/components/Pagination/index.ts | 1 - .../ui/components/{Spinner => }/Spinner.tsx | 5 +- .../ui/components/Spinner/Spinner.module.css | 23 - .../src/ui/components/Spinner/index.ts | 1 - .../ui/pages/{Character => }/Character.tsx | 15 +- .../ui/pages/Character/Character.module.css | 134 -- .../src/ui/pages/Character/index.ts | 2 - .../ui/pages/{Characters => }/Characters.tsx | 9 +- .../ui/pages/Characters/Characters.module.css | 72 - .../src/ui/pages/Characters/index.ts | 2 - .../src/ui/pages/{Episode => }/Episode.tsx | 15 +- .../src/ui/pages/Episode/Episode.module.css | 74 - .../src/ui/pages/Episode/index.ts | 2 - .../src/ui/pages}/Episodes.tsx | 9 +- .../src/ui/pages/Episodes/Episodes.module.css | 72 - .../src/ui/pages/Episodes/index.ts | 2 - .../src/ui/pages/{Location => }/Location.tsx | 15 +- .../src/ui/pages/Location/Location.module.css | 130 -- .../src/ui/pages/Location/index.ts | 2 - .../ui/pages/{Locations => }/Locations.tsx | 9 +- .../ui/pages/Locations/Locations.module.css | 72 - .../src/ui/pages/Locations/index.ts | 2 - .../ui/pages/{MainLayout => }/MainLayout.tsx | 21 +- .../ui/pages/MainLayout/MainLayout.module.css | 133 -- .../src/ui/pages/MainLayout/index.ts | 1 - .../svelte-nano_kit/src/common/constants.ts | 1 + .../svelte-nano_kit/src/services/api.ts | 108 +- .../svelte-nano_kit/src/services/types.ts | 10 - .../svelte-nano_kit/src/stores/characters.ts | 48 +- .../svelte-nano_kit/src/stores/episodes.ts | 36 +- .../svelte-nano_kit/src/stores/locations.ts | 4 +- examples/weather/common/eslint.config.js | 22 + examples/weather/common/package.json | 20 + .../weather/common/src/nanostores/location.ts | 64 + .../weather/common/src/nanostores/query.ts | 7 + .../weather/common/src/nanostores/weather.ts | 39 + .../weather/common/src/services/cities.ts | 100 + .../weather/common/src/services/location.ts | 22 + examples/weather/common/src/services/types.ts | 27 + .../weather/common/src/services/weather.ts | 208 ++ .../weather/common/src/stores-di/location.ts | 56 + .../weather/common/src/stores-di/query.ts | 12 + .../weather/common/src/stores-di/weather.ts | 56 + .../weather/common/src/stores/location.ts | 48 + examples/weather/common/src/stores/query.ts | 10 + examples/weather/common/src/stores/weather.ts | 37 + examples/weather/common/tsconfig.json | 28 + .../nanoviews-nano_kit-di/src/App.module.css | 20 - .../weather/nanoviews-nano_kit-di/src/App.ts | 5 +- .../weather/nanoviews-nano_kit-di/src/app.css | 258 ++- .../src/components/Autocomplete.ts | 131 ++ .../src/components/CityInput.module.css | 27 - .../src/components/CityInput.ts | 48 +- .../src/components/Forecast.module.css | 24 - .../src/components/Forecast.ts | 12 +- .../src/components/ForecastWeather.module.css | 48 - .../src/components/ForecastWeather.ts | 11 +- .../src/components/Weather.module.css | 56 - .../src/components/Weather.ts | 11 +- .../src/services/cities.ts | 2 +- .../src/services/types.ts | 3 + .../src/services/weather.ts | 49 +- .../nanoviews-nano_kit/src/App.module.css | 20 - .../weather/nanoviews-nano_kit/src/App.ts | 5 +- .../weather/nanoviews-nano_kit/src/app.css | 258 ++- .../src/components/Autocomplete.ts | 131 ++ .../src/components/CityInput.module.css | 27 - .../src/components/CityInput.ts | 46 +- .../src/components/Forecast.module.css | 24 - .../src/components/Forecast.ts | 12 +- .../src/components/ForecastWeather.module.css | 48 - .../src/components/ForecastWeather.ts | 11 +- .../src/components/Weather.module.css | 56 - .../src/components/Weather.ts | 11 +- .../nanoviews-nano_kit/src/services/cities.ts | 2 +- .../nanoviews-nano_kit/src/services/types.ts | 3 + .../src/services/weather.ts | 49 +- .../react-nano_kit-di/src/App.module.css | 20 - .../weather/react-nano_kit-di/src/App.tsx | 5 +- .../weather/react-nano_kit-di/src/app.css | 274 ++- .../src/components/Autocomplete.tsx | 158 ++ .../src/components/CityInput.module.css | 27 - .../src/components/CityInput.tsx | 40 +- .../src/components/Forecast.module.css | 24 - .../src/components/Forecast.tsx | 12 +- .../src/components/ForecastWeather.module.css | 48 - .../src/components/ForecastWeather.tsx | 11 +- .../src/components/Weather.module.css | 56 - .../src/components/Weather.tsx | 11 +- .../react-nano_kit-di/src/services/cities.ts | 2 +- .../react-nano_kit-di/src/services/types.ts | 3 + .../react-nano_kit-di/src/services/weather.ts | 49 +- .../weather/react-nano_kit/src/App.module.css | 20 - examples/weather/react-nano_kit/src/App.tsx | 5 +- examples/weather/react-nano_kit/src/app.css | 258 ++- .../src/components/Autocomplete.tsx | 158 ++ .../src/components/CityInput.module.css | 27 - .../src/components/CityInput.tsx | 40 +- .../src/components/Forecast.module.css | 24 - .../src/components/Forecast.tsx | 12 +- .../src/components/ForecastWeather.module.css | 48 - .../src/components/ForecastWeather.tsx | 11 +- .../src/components/Weather.module.css | 56 - .../react-nano_kit/src/components/Weather.tsx | 11 +- .../react-nano_kit/src/services/types.ts | 3 + .../react-nano_kit/src/services/weather.ts | 49 +- .../react-nanostores/src/App.module.css | 20 - examples/weather/react-nanostores/src/App.tsx | 5 +- examples/weather/react-nanostores/src/app.css | 258 ++- .../src/components/Autocomplete.tsx | 158 ++ .../src/components/CityInput.module.css | 27 - .../src/components/CityInput.tsx | 37 +- .../src/components/Forecast.module.css | 24 - .../src/components/Forecast.tsx | 12 +- .../src/components/ForecastWeather.module.css | 48 - .../src/components/ForecastWeather.tsx | 11 +- .../src/components/Weather.module.css | 56 - .../src/components/Weather.tsx | 11 +- .../react-nanostores/src/services/cities.ts | 2 +- .../react-nanostores/src/services/types.ts | 3 + .../react-nanostores/src/services/weather.ts | 49 +- .../weather/react-reatom/src/App.module.css | 20 - examples/weather/react-reatom/src/App.tsx | 5 +- examples/weather/react-reatom/src/app.css | 258 ++- .../src/components/Autocomplete.tsx | 158 ++ .../src/components/CityInput.module.css | 27 - .../react-reatom/src/components/CityInput.tsx | 40 +- .../src/components/Forecast.module.css | 24 - .../react-reatom/src/components/Forecast.tsx | 12 +- .../src/components/ForecastWeather.module.css | 48 - .../src/components/ForecastWeather.tsx | 11 +- .../src/components/Weather.module.css | 56 - .../react-reatom/src/components/Weather.tsx | 11 +- .../react-reatom/src/services/types.ts | 3 + .../react-reatom/src/services/weather.ts | 49 +- .../weather/react-tanstack/src/App.module.css | 20 - examples/weather/react-tanstack/src/App.tsx | 5 +- examples/weather/react-tanstack/src/app.css | 258 ++- .../src/components/Autocomplete.tsx | 158 ++ .../src/components/CityInput.module.css | 27 - .../src/components/CityInput.tsx | 40 +- .../src/components/Forecast.module.css | 24 - .../src/components/Forecast.tsx | 12 +- .../src/components/ForecastWeather.module.css | 48 - .../src/components/ForecastWeather.tsx | 11 +- .../src/components/Weather.module.css | 56 - .../react-tanstack/src/components/Weather.tsx | 11 +- .../react-tanstack/src/services/cities.ts | 2 +- .../react-tanstack/src/services/types.ts | 3 + .../react-tanstack/src/services/weather.ts | 49 +- .../solid-nanostores/src/App.module.css | 20 - examples/weather/solid-nanostores/src/App.tsx | 5 +- examples/weather/solid-nanostores/src/app.css | 258 ++- .../src/components/Autocomplete.tsx | 110 + .../src/components/CityInput.module.css | 27 - .../src/components/CityInput.tsx | 33 +- .../src/components/Forecast.module.css | 24 - .../src/components/Forecast.tsx | 12 +- .../src/components/ForecastWeather.module.css | 48 - .../src/components/ForecastWeather.tsx | 11 +- .../src/components/Weather.module.css | 56 - .../src/components/Weather.tsx | 11 +- .../solid-nanostores/src/services/cities.ts | 2 +- .../solid-nanostores/src/services/types.ts | 3 + .../solid-nanostores/src/services/weather.ts | 49 +- .../weather/svelte-nano_kit-di/src/App.svelte | 27 +- .../weather/svelte-nano_kit-di/src/app.css | 258 ++- .../src/components/Autocomplete.svelte | 100 + .../src/components/CityInput.svelte | 55 +- .../src/components/Forecast.svelte | 40 +- .../src/components/ForecastWeather.svelte | 56 +- .../src/components/Weather.svelte | 69 +- .../svelte-nano_kit-di/src/services/types.ts | 3 + .../src/services/weather.ts | 49 +- .../weather/svelte-nano_kit/src/App.svelte | 27 +- examples/weather/svelte-nano_kit/src/app.css | 258 ++- .../src/components/Autocomplete.svelte | 100 + .../src/components/CityInput.svelte | 55 +- .../src/components/Forecast.svelte | 40 +- .../src/components/ForecastWeather.svelte | 56 +- .../src/components/Weather.svelte | 69 +- .../svelte-nano_kit/src/services/types.ts | 3 + .../svelte-nano_kit/src/services/weather.ts | 49 +- .../weather/svelte-nanostores/package.json | 2 +- .../weather/svelte-nanostores/src/App.svelte | 27 +- .../weather/svelte-nanostores/src/app.css | 258 ++- .../src/components/Autocomplete.svelte | 100 + .../src/components/CityInput.svelte | 55 +- .../src/components/Forecast.svelte | 40 +- .../src/components/ForecastWeather.svelte | 56 +- .../src/components/Weather.svelte | 69 +- .../svelte-nanostores/src/services/cities.ts | 2 +- .../svelte-nanostores/src/services/types.ts | 3 + .../svelte-nanostores/src/services/weather.ts | 49 +- .../svelte-nanostores/tsconfig.node.json | 1 - package.json | 1 + pnpm-lock.yaml | 72 + scripts/constants.js | 2 - scripts/examples-sync.js | 116 ++ scripts/index.js | 1 - 648 files changed, 20618 insertions(+), 15931 deletions(-) create mode 100644 examples/event-board/common/api/events.js create mode 100644 examples/event-board/common/api/index.js create mode 100644 examples/event-board/common/api/server.js create mode 100644 examples/event-board/common/eslint.config.js create mode 100644 examples/event-board/common/package.json create mode 100644 examples/event-board/common/src/routes.ts create mode 100644 examples/event-board/common/src/services/events.ts create mode 100644 examples/event-board/common/src/services/events.types.ts create mode 100644 examples/event-board/common/src/stores-di/events.ts create mode 100644 examples/event-board/common/src/stores-di/query.ts create mode 100644 examples/event-board/common/src/stores-di/router.ts create mode 100644 examples/event-board/common/src/stores-di/utils.ts create mode 100644 examples/event-board/common/tsconfig.json create mode 100644 examples/rick-and-morty/common/eslint.config.js create mode 100644 examples/rick-and-morty/common/package.json create mode 100644 examples/rick-and-morty/common/src/common/constants.ts create mode 100644 examples/rick-and-morty/common/src/routes.ts create mode 100644 examples/rick-and-morty/common/src/services/api.ts create mode 100644 examples/rick-and-morty/common/src/services/types.ts create mode 100644 examples/rick-and-morty/common/src/stores-di/characters.ts create mode 100644 examples/rick-and-morty/common/src/stores-di/episodes.ts create mode 100644 examples/rick-and-morty/common/src/stores-di/locations.ts create mode 100644 examples/rick-and-morty/common/src/stores-di/memoryStorage.ts create mode 100644 examples/rick-and-morty/common/src/stores-di/query.ts create mode 100644 examples/rick-and-morty/common/src/stores-di/router.ts create mode 100644 examples/rick-and-morty/common/src/stores/characters.ts create mode 100644 examples/rick-and-morty/common/src/stores/episodes.ts create mode 100644 examples/rick-and-morty/common/src/stores/locations.ts create mode 100644 examples/rick-and-morty/common/src/stores/query.ts create mode 100644 examples/rick-and-morty/common/src/stores/router.ts create mode 100644 examples/rick-and-morty/common/tsconfig.json delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/app/layout.module.css create mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/index.ts rename examples/rick-and-morty/{next-pages-nano_kit-ssr/src/ui/blocks/CharacterDetail => next-app-nano_kit-ssr/src/ui/blocks}/CharacterDetail.tsx (52%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/{CharactersGrid => }/CharactersGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharactersGrid/CharactersGrid.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharactersGrid/index.ts create mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/index.ts create mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/index.ts rename examples/rick-and-morty/{next-pages-nano_kit-ssr/src/ui/blocks/EpisodesGrid => next-app-nano_kit-ssr/src/ui/blocks}/EpisodesGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodesGrid/EpisodesGrid.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodesGrid/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/{Header => }/Header.tsx (57%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/Header/Header.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/Header/index.ts create mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationCard.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationCard/LocationCard.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationCard/LocationCard.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationCard/index.ts create mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationDetail/LocationDetail.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationDetail/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationDetail/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/{LocationsGrid => }/LocationsGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationsGrid/LocationsGrid.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/LocationsGrid/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/components/{Pagination => }/Pagination.tsx (85%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/components/Pagination/Pagination.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/components/Pagination/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/components/{Spinner => }/Spinner.tsx (59%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/components/Spinner/Spinner.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/components/Spinner/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Character/stores.ts => Character.stores.ts} (100%) rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Character => }/Character.tsx (82%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Character/Character.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Character/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Characters/stores.ts => Characters.stores.ts} (100%) rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Characters => }/Characters.tsx (88%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Characters/Characters.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Characters/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Episode/stores.ts => Episode.stores.ts} (100%) rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Episode => }/Episode.tsx (81%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Episode/Episode.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Episode/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Episodes/stores.ts => Episodes.stores.ts} (100%) rename examples/rick-and-morty/{next-pages-nano_kit-ssr/src/ui/pages/Episodes => next-app-nano_kit-ssr/src/ui/pages}/Episodes.tsx (87%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Episodes/Episodes.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Episodes/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Location/stores.ts => Location.stores.ts} (100%) rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Location => }/Location.tsx (82%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Location/Location.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Location/index.ts rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Locations/stores.ts => Locations.stores.ts} (100%) rename examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/{Locations => }/Locations.tsx (87%) delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Locations/Locations.module.css delete mode 100644 examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/pages/Locations/index.ts delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/styles/layout.module.css create mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterCard/index.ts rename examples/rick-and-morty/{next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail => next-pages-nano_kit-ssr/src/ui/blocks}/CharacterDetail.tsx (52%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterDetail/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/{CharactersGrid => }/CharactersGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharactersGrid/CharactersGrid.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharactersGrid/index.ts create mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodeCard/index.ts create mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodeDetail/index.ts rename examples/rick-and-morty/{next-app-nano_kit-ssr/src/ui/blocks/EpisodesGrid => next-pages-nano_kit-ssr/src/ui/blocks}/EpisodesGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodesGrid/EpisodesGrid.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodesGrid/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/{Header => }/Header.tsx (60%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/Header/Header.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/Header/index.ts create mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationCard.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationCard/LocationCard.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationCard/LocationCard.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationCard/index.ts create mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationDetail/LocationDetail.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationDetail/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationDetail/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/{LocationsGrid => }/LocationsGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationsGrid/LocationsGrid.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/LocationsGrid/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/components/{Pagination => }/Pagination.tsx (84%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/components/Pagination/Pagination.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/components/Pagination/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/components/{Spinner => }/Spinner.tsx (59%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/components/Spinner/Spinner.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/components/Spinner/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Character/stores.ts => Character.stores.ts} (100%) rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Character => }/Character.tsx (82%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Character/Character.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Character/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Characters/stores.ts => Characters.stores.ts} (100%) rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Characters => }/Characters.tsx (88%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Characters/Characters.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Characters/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Episode/stores.ts => Episode.stores.ts} (100%) rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Episode => }/Episode.tsx (81%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Episode/Episode.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Episode/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Episodes/stores.ts => Episodes.stores.ts} (100%) rename examples/rick-and-morty/{next-app-nano_kit-ssr/src/ui/pages/Episodes => next-pages-nano_kit-ssr/src/ui/pages}/Episodes.tsx (87%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Episodes/Episodes.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Episodes/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Location/stores.ts => Location.stores.ts} (100%) rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Location => }/Location.tsx (82%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Location/Location.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Location/index.ts rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Locations/stores.ts => Locations.stores.ts} (100%) rename examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/{Locations => }/Locations.tsx (87%) delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Locations/Locations.module.css delete mode 100644 examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/pages/Locations/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/CharacterCard/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/{CharacterDetail => }/CharacterDetail.tsx (51%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/CharacterDetail/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/{CharactersGrid => }/CharactersGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/CharactersGrid/CharactersGrid.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/CharactersGrid/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodeCard/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodeDetail/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/{EpisodesGrid => }/EpisodesGrid.tsx (83%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodesGrid/EpisodesGrid.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/EpisodesGrid/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationCard/LocationCard.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationCard/LocationCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationCard/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationDetail/LocationDetail.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationDetail/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationDetail/index.ts rename examples/rick-and-morty/{react-tanstack-start-ssr/src/ui/blocks/LocationsGrid => react-nano_kit-ssr/src/ui/blocks}/LocationsGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationsGrid/LocationsGrid.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/blocks/LocationsGrid/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/components/{Pagination => }/Pagination.tsx (85%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/components/Pagination/Pagination.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/components/Pagination/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/components/{Spinner => }/Spinner.tsx (57%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/components/Spinner/Spinner.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/components/Spinner/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/{Character => }/Character.tsx (86%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Character/Character.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Character/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/{Characters => }/Characters.tsx (89%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Characters/Characters.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Characters/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/{Episode => }/Episode.tsx (86%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Episode/Episode.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Episode/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/{Episodes => }/Episodes.tsx (89%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Episodes/Episodes.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Episodes/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/{Home => }/Home.tsx (87%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Home/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/{Location => }/Location.tsx (86%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Location/Location.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Location/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/{Locations => }/Locations.tsx (89%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Locations/Locations.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/Locations/index.ts rename examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/{MainLayout => }/MainLayout.tsx (59%) delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/MainLayout/MainLayout.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit-ssr/src/ui/pages/MainLayout/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharacterCard/CharacterCard.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharacterCard/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharacterCard/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharacterDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharacterDetail/CharacterDetail.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharacterDetail/CharacterDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharacterDetail/index.ts rename examples/rick-and-morty/{react-tanstack-start-ssr/src/ui/blocks/CharactersGrid => react-nano_kit/src/ui/blocks}/CharactersGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharactersGrid/CharactersGrid.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/CharactersGrid/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodeCard/EpisodeCard.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodeCard/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodeCard/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodeDetail/index.ts rename examples/rick-and-morty/{react-tanstack-start-ssr/src/ui/blocks/EpisodesGrid => react-nano_kit/src/ui/blocks}/EpisodesGrid.tsx (83%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodesGrid/EpisodesGrid.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/EpisodesGrid/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationCard/LocationCard.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationCard/LocationCard.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationCard/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationDetail/LocationDetail.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationDetail/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationDetail/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/blocks/{LocationsGrid => }/LocationsGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationsGrid/LocationsGrid.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/blocks/LocationsGrid/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/components/{Link => }/Link.tsx (100%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/components/Link/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/components/{Pagination => }/Pagination.tsx (85%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/components/Pagination/Pagination.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/components/Pagination/index.ts rename examples/rick-and-morty/{react-tanstack-start-ssr/src/ui/components/Spinner => react-nano_kit/src/ui/components}/Spinner.tsx (57%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/components/Spinner/Spinner.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/components/Spinner/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/pages/{Character => }/Character.tsx (77%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Character/Character.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Character/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/pages/{Characters => }/Characters.tsx (85%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Characters/Characters.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Characters/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/pages/{Episode => }/Episode.tsx (77%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Episode/Episode.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Episode/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/pages/{Episodes => }/Episodes.tsx (85%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Episodes/Episodes.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Episodes/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/pages/{Home => }/Home.tsx (76%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Home/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/pages/{Location => }/Location.tsx (77%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Location/Location.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Location/index.ts rename examples/rick-and-morty/react-nano_kit/src/ui/pages/{Locations => }/Locations.tsx (85%) delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Locations/Locations.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/Locations/index.ts create mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/MainLayout.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/MainLayout/MainLayout.module.css delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/MainLayout/MainLayout.tsx delete mode 100644 examples/rick-and-morty/react-nano_kit/src/ui/pages/MainLayout/index.ts create mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharacterCard/CharacterCard.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharacterCard/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharacterCard/index.ts create mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharacterDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharacterDetail/index.ts rename examples/rick-and-morty/{react-tanstack/src/ui/blocks/CharactersGrid => react-tanstack-start-ssr/src/ui/blocks}/CharactersGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharactersGrid/CharactersGrid.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/CharactersGrid/index.ts create mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodeCard/index.ts create mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodeDetail/index.ts rename examples/rick-and-morty/{react-nano_kit/src/ui/blocks/EpisodesGrid => react-tanstack-start-ssr/src/ui/blocks}/EpisodesGrid.tsx (83%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodesGrid/EpisodesGrid.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/EpisodesGrid/index.ts create mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationCard/LocationCard.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationCard/LocationCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationCard/index.ts create mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationDetail/LocationDetail.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationDetail/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationDetail/index.ts rename examples/rick-and-morty/{react-tanstack/src/ui/blocks/LocationsGrid => react-tanstack-start-ssr/src/ui/blocks}/LocationsGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationsGrid/LocationsGrid.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/blocks/LocationsGrid/index.ts rename examples/rick-and-morty/{react-tanstack/src/ui/components/Pagination => react-tanstack-start-ssr/src/ui/components}/Pagination.tsx (85%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/components/Pagination/Pagination.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/components/Pagination/index.ts rename examples/rick-and-morty/{react-nano_kit/src/ui/components/Spinner => react-tanstack-start-ssr/src/ui/components}/Spinner.tsx (57%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/components/Spinner/Spinner.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/components/Spinner/index.ts rename examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/{Character => }/Character.tsx (72%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Character/Character.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Character/index.ts rename examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/{Characters => }/Characters.tsx (85%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Characters/Characters.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Characters/index.ts rename examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/{Episode => }/Episode.tsx (72%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Episode/Episode.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Episode/index.ts rename examples/rick-and-morty/{react-tanstack/src/ui/pages/Episodes => react-tanstack-start-ssr/src/ui/pages}/Episodes.tsx (85%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Episodes/Episodes.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Episodes/index.ts rename examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/{Location => }/Location.tsx (73%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Location/Location.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Location/index.ts rename examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/{Locations => }/Locations.tsx (85%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Locations/Locations.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/Locations/index.ts rename examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/{MainLayout => }/MainLayout.tsx (55%) delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/MainLayout/MainLayout.module.css delete mode 100644 examples/rick-and-morty/react-tanstack-start-ssr/src/ui/pages/MainLayout/index.ts create mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharacterCard/CharacterCard.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharacterCard/CharacterCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharacterCard/index.ts create mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharacterDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharacterDetail/CharacterDetail.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharacterDetail/CharacterDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharacterDetail/index.ts rename examples/rick-and-morty/{react-nano_kit/src/ui/blocks/CharactersGrid => react-tanstack/src/ui/blocks}/CharactersGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharactersGrid/CharactersGrid.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/CharactersGrid/index.ts create mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodeCard/EpisodeCard.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodeCard/EpisodeCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodeCard/index.ts create mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodeDetail/index.ts rename examples/rick-and-morty/react-tanstack/src/ui/blocks/{EpisodesGrid => }/EpisodesGrid.tsx (83%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodesGrid/EpisodesGrid.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/EpisodesGrid/index.ts create mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationCard/LocationCard.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationCard/LocationCard.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationCard/index.ts create mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationDetail/LocationDetail.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationDetail/LocationDetail.tsx delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationDetail/index.ts rename examples/rick-and-morty/{react-nano_kit-ssr/src/ui/blocks/LocationsGrid => react-tanstack/src/ui/blocks}/LocationsGrid.tsx (84%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationsGrid/LocationsGrid.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/blocks/LocationsGrid/index.ts rename examples/rick-and-morty/{react-tanstack-start-ssr/src/ui/components/Pagination => react-tanstack/src/ui/components}/Pagination.tsx (85%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/components/Pagination/Pagination.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/components/Pagination/index.ts rename examples/rick-and-morty/react-tanstack/src/ui/components/{Spinner => }/Spinner.tsx (57%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/components/Spinner/Spinner.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/components/Spinner/index.ts rename examples/rick-and-morty/react-tanstack/src/ui/pages/{Character => }/Character.tsx (72%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Character/Character.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Character/index.ts rename examples/rick-and-morty/react-tanstack/src/ui/pages/{Characters => }/Characters.tsx (85%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Characters/Characters.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Characters/index.ts rename examples/rick-and-morty/react-tanstack/src/ui/pages/{Episode => }/Episode.tsx (72%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Episode/Episode.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Episode/index.ts rename examples/rick-and-morty/{react-tanstack-start-ssr/src/ui/pages/Episodes => react-tanstack/src/ui/pages}/Episodes.tsx (85%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Episodes/Episodes.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Episodes/index.ts rename examples/rick-and-morty/react-tanstack/src/ui/pages/{Location => }/Location.tsx (73%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Location/Location.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Location/index.ts rename examples/rick-and-morty/react-tanstack/src/ui/pages/{Locations => }/Locations.tsx (85%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Locations/Locations.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/Locations/index.ts rename examples/rick-and-morty/react-tanstack/src/ui/pages/{MainLayout => }/MainLayout.tsx (55%) delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/MainLayout/MainLayout.module.css delete mode 100644 examples/rick-and-morty/react-tanstack/src/ui/pages/MainLayout/index.ts create mode 100644 examples/weather/common/eslint.config.js create mode 100644 examples/weather/common/package.json create mode 100644 examples/weather/common/src/nanostores/location.ts create mode 100644 examples/weather/common/src/nanostores/query.ts create mode 100644 examples/weather/common/src/nanostores/weather.ts create mode 100644 examples/weather/common/src/services/cities.ts create mode 100644 examples/weather/common/src/services/location.ts create mode 100644 examples/weather/common/src/services/types.ts create mode 100644 examples/weather/common/src/services/weather.ts create mode 100644 examples/weather/common/src/stores-di/location.ts create mode 100644 examples/weather/common/src/stores-di/query.ts create mode 100644 examples/weather/common/src/stores-di/weather.ts create mode 100644 examples/weather/common/src/stores/location.ts create mode 100644 examples/weather/common/src/stores/query.ts create mode 100644 examples/weather/common/src/stores/weather.ts create mode 100644 examples/weather/common/tsconfig.json delete mode 100644 examples/weather/nanoviews-nano_kit-di/src/App.module.css create mode 100644 examples/weather/nanoviews-nano_kit-di/src/components/Autocomplete.ts delete mode 100644 examples/weather/nanoviews-nano_kit-di/src/components/CityInput.module.css delete mode 100644 examples/weather/nanoviews-nano_kit-di/src/components/Forecast.module.css delete mode 100644 examples/weather/nanoviews-nano_kit-di/src/components/ForecastWeather.module.css delete mode 100644 examples/weather/nanoviews-nano_kit-di/src/components/Weather.module.css delete mode 100644 examples/weather/nanoviews-nano_kit/src/App.module.css create mode 100644 examples/weather/nanoviews-nano_kit/src/components/Autocomplete.ts delete mode 100644 examples/weather/nanoviews-nano_kit/src/components/CityInput.module.css delete mode 100644 examples/weather/nanoviews-nano_kit/src/components/Forecast.module.css delete mode 100644 examples/weather/nanoviews-nano_kit/src/components/ForecastWeather.module.css delete mode 100644 examples/weather/nanoviews-nano_kit/src/components/Weather.module.css delete mode 100644 examples/weather/react-nano_kit-di/src/App.module.css create mode 100644 examples/weather/react-nano_kit-di/src/components/Autocomplete.tsx delete mode 100644 examples/weather/react-nano_kit-di/src/components/CityInput.module.css delete mode 100644 examples/weather/react-nano_kit-di/src/components/Forecast.module.css delete mode 100644 examples/weather/react-nano_kit-di/src/components/ForecastWeather.module.css delete mode 100644 examples/weather/react-nano_kit-di/src/components/Weather.module.css delete mode 100644 examples/weather/react-nano_kit/src/App.module.css create mode 100644 examples/weather/react-nano_kit/src/components/Autocomplete.tsx delete mode 100644 examples/weather/react-nano_kit/src/components/CityInput.module.css delete mode 100644 examples/weather/react-nano_kit/src/components/Forecast.module.css delete mode 100644 examples/weather/react-nano_kit/src/components/ForecastWeather.module.css delete mode 100644 examples/weather/react-nano_kit/src/components/Weather.module.css delete mode 100644 examples/weather/react-nanostores/src/App.module.css create mode 100644 examples/weather/react-nanostores/src/components/Autocomplete.tsx delete mode 100644 examples/weather/react-nanostores/src/components/CityInput.module.css delete mode 100644 examples/weather/react-nanostores/src/components/Forecast.module.css delete mode 100644 examples/weather/react-nanostores/src/components/ForecastWeather.module.css delete mode 100644 examples/weather/react-nanostores/src/components/Weather.module.css delete mode 100644 examples/weather/react-reatom/src/App.module.css create mode 100644 examples/weather/react-reatom/src/components/Autocomplete.tsx delete mode 100644 examples/weather/react-reatom/src/components/CityInput.module.css delete mode 100644 examples/weather/react-reatom/src/components/Forecast.module.css delete mode 100644 examples/weather/react-reatom/src/components/ForecastWeather.module.css delete mode 100644 examples/weather/react-reatom/src/components/Weather.module.css delete mode 100644 examples/weather/react-tanstack/src/App.module.css create mode 100644 examples/weather/react-tanstack/src/components/Autocomplete.tsx delete mode 100644 examples/weather/react-tanstack/src/components/CityInput.module.css delete mode 100644 examples/weather/react-tanstack/src/components/Forecast.module.css delete mode 100644 examples/weather/react-tanstack/src/components/ForecastWeather.module.css delete mode 100644 examples/weather/react-tanstack/src/components/Weather.module.css delete mode 100644 examples/weather/solid-nanostores/src/App.module.css create mode 100644 examples/weather/solid-nanostores/src/components/Autocomplete.tsx delete mode 100644 examples/weather/solid-nanostores/src/components/CityInput.module.css delete mode 100644 examples/weather/solid-nanostores/src/components/Forecast.module.css delete mode 100644 examples/weather/solid-nanostores/src/components/ForecastWeather.module.css delete mode 100644 examples/weather/solid-nanostores/src/components/Weather.module.css create mode 100644 examples/weather/svelte-nano_kit-di/src/components/Autocomplete.svelte create mode 100644 examples/weather/svelte-nano_kit/src/components/Autocomplete.svelte create mode 100644 examples/weather/svelte-nanostores/src/components/Autocomplete.svelte delete mode 100644 scripts/constants.js create mode 100644 scripts/examples-sync.js delete mode 100644 scripts/index.js diff --git a/.gitignore b/.gitignore index a039cfc2..785662cc 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ node_modules # Some caches .*cache .trash -tsconfig.tsbuildinfo +*.tsbuildinfo # Compiled dist dist diff --git a/examples/event-board/common/api/events.js b/examples/event-board/common/api/events.js new file mode 100644 index 00000000..c14e7bf1 --- /dev/null +++ b/examples/event-board/common/api/events.js @@ -0,0 +1,262 @@ +/** + * Event categories supported by the mock API. + */ +export const eventCategories = [ + 'conference', + 'meetup', + 'workshop', + 'webinar' +] + +const DEFAULT_LIMIT = 3 +const MAX_LIMIT = 20 +const events = [ + { + id: '1', + slug: 'react-ssr-workshop', + title: 'React SSR Workshop', + description: 'A hands-on workshop about server rendering, hydration, and app architecture.', + startsAt: new Date('2026-05-12T18:00:00Z').getTime(), + location: 'Online', + category: 'workshop', + attendees: 24 + }, + { + id: '2', + slug: 'frontend-meetup-spring', + title: 'Frontend Meetup: Spring Edition', + description: 'Short talks about modern frontend tooling, routing, and state management.', + startsAt: new Date('2026-05-21T19:30:00Z').getTime(), + location: 'Berlin', + category: 'meetup', + attendees: 58 + }, + { + id: '3', + slug: 'state-management-webinar', + title: 'State Management Webinar', + description: 'A practical session on signals, derived state, and data fetching.', + startsAt: new Date('2026-06-03T17:00:00Z').getTime(), + location: 'Online', + category: 'webinar', + attendees: 102 + }, + { + id: '4', + slug: 'vite-plugin-night', + title: 'Vite Plugin Night', + description: 'A meetup about Vite plugins, build pipelines, and developer experience.', + startsAt: new Date('2026-06-11T18:30:00Z').getTime(), + location: 'Prague', + category: 'meetup', + attendees: 41 + }, + { + id: '5', + slug: 'web-platform-conference', + title: 'Web Platform Conference', + description: 'A one-day conference about browser APIs, performance, and modern web apps.', + startsAt: new Date('2026-06-24T09:00:00Z').getTime(), + location: 'Amsterdam', + category: 'conference', + attendees: 180 + }, + { + id: '6', + slug: 'hydration-deep-dive', + title: 'Hydration Deep Dive', + description: 'A technical webinar about SSR, RSC, cache hydration, and client handoff.', + startsAt: new Date('2026-07-02T16:00:00Z').getTime(), + location: 'Online', + category: 'webinar', + attendees: 76 + } +] + +function normalizeLimit(value) { + const limit = Number(value || DEFAULT_LIMIT) + + if (!Number.isFinite(limit) || limit < 1) { + return DEFAULT_LIMIT + } + + return Math.min(limit, MAX_LIMIT) +} + +function normalizeCategory(value) { + if (!value) { + return null + } + + return eventCategories.includes(value) ? value : undefined +} + +function slugify(value) { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') +} + +function uniqueSlug(title) { + const base = slugify(title) || 'event' + let slug = base + let index = 2 + + while (findEvent(slug)) { + slug = `${base}-${index}` + index += 1 + } + + return slug +} + +function sortEvents(items) { + return [...items].sort((a, b) => a.startsAt - b.startsAt || a.id.localeCompare(b.id)) +} + +function validateEventInput(input) { + const errors = {} + + if (!input || typeof input !== 'object') { + return { + form: 'Expected event payload' + } + } + + if (typeof input.title !== 'string' || !input.title.trim()) { + errors.title = 'Title is required' + } + + if (typeof input.description !== 'string' || !input.description.trim()) { + errors.description = 'Description is required' + } + + if (typeof input.startsAt !== 'number' || !Number.isFinite(input.startsAt)) { + errors.startsAt = 'Date and time are required' + } + + if (typeof input.location !== 'string' || !input.location.trim()) { + errors.location = 'Location is required' + } + + if (!eventCategories.includes(input.category)) { + errors.category = 'Category is invalid' + } + + return errors +} + +/** + * Return a filtered and cursor-paginated page of events. + * @param {object} query - Query values from the HTTP request. + * @param {string | undefined} query.q - Search text. + * @param {string | undefined} query.category - Category filter. + * @param {string | undefined} query.cursor - Anchor cursor based on `startsAt`. + * @param {string | undefined} query.limit - Page size. + * @returns {{ status: number, body: { events: object[], nextCursor?: number } | { error: string } }} API response payload. + */ +export function listEvents(query) { + const q = query.q?.trim().toLowerCase() + const category = normalizeCategory(query.category) + const cursor = Number(query.cursor || 0) + const limit = normalizeLimit(query.limit) + + if (category === undefined) { + return { + status: 400, + body: { + error: 'Unknown event category' + } + } + } + + const filtered = sortEvents(events).filter((event) => { + if (category && event.category !== category) { + return false + } + + if (Number.isFinite(cursor) && cursor > 0 && event.startsAt <= cursor) { + return false + } + + if (!q) { + return true + } + + return `${event.title} ${event.description} ${event.location}`.toLowerCase().includes(q) + }) + const pageItems = filtered.slice(0, limit) + const hasMore = filtered.length > pageItems.length + + return { + status: 200, + body: { + events: pageItems, + nextCursor: hasMore ? pageItems[pageItems.length - 1]?.startsAt : undefined + } + } +} + +/** + * Find an event by slug. + * @param {string} slug - Event slug. + * @returns {object | null} Event object or `null` when it does not exist. + */ +export function findEvent(slug) { + return events.find(event => event.slug === slug) || null +} + +/** + * Create a new event in the in-memory store. + * @param {object} input - Event form payload. + * @returns {{ status: number, body: object }} API response payload. + */ +export function createEvent(input) { + const errors = validateEventInput(input) + + if (Object.keys(errors).length > 0) { + return { + status: 400, + body: { + errors + } + } + } + + const event = { + id: crypto.randomUUID(), + slug: uniqueSlug(input.title), + title: input.title.trim(), + description: input.description.trim(), + startsAt: input.startsAt, + location: input.location.trim(), + category: input.category, + attendees: 0 + } + + events.push(event) + + return { + status: 201, + body: event + } +} + +/** + * Increment attendees count for an event. + * @param {string} id - Event id. + * @returns {object | null} Updated event object or `null` when it does not exist. + */ +export function rsvpEvent(id) { + const event = events.find(item => item.id === id) || null + + if (!event) { + return null + } + + event.attendees += 1 + + return event +} diff --git a/examples/event-board/common/api/index.js b/examples/event-board/common/api/index.js new file mode 100644 index 00000000..56bb368b --- /dev/null +++ b/examples/event-board/common/api/index.js @@ -0,0 +1,71 @@ +import { Hono } from 'hono' +import { + createEvent, + findEvent, + listEvents, + rsvpEvent +} from './events.js' + +const HTTP_BAD_REQUEST = 400 +const HTTP_NOT_FOUND = 404 + +export function createApiApp() { + const app = new Hono() + + app.get('/api/events', (c) => { + const result = listEvents({ + q: c.req.query('q'), + category: c.req.query('category'), + cursor: c.req.query('cursor'), + limit: c.req.query('limit') + }) + + return c.json(result.body, result.status) + }) + + app.get('/api/events/:slug', (c) => { + const event = findEvent(c.req.param('slug')) + + if (!event) { + return c.json(null, HTTP_NOT_FOUND) + } + + return c.json(event) + }) + + app.post('/api/events', async (c) => { + let body + + try { + body = await c.req.json() + } catch { + return c.json({ + errors: { + form: 'Expected JSON payload' + } + }, HTTP_BAD_REQUEST) + } + + const result = createEvent(body) + + return c.json(result.body, result.status) + }) + + app.post('/api/events/:id/rsvp', (c) => { + const event = rsvpEvent(c.req.param('id')) + + if (!event) { + return c.json(null, HTTP_NOT_FOUND) + } + + return c.json(event) + }) + + return app +} + +export function api() { + const app = createApiApp() + + return c => app.fetch(c.req.raw) +} diff --git a/examples/event-board/common/api/server.js b/examples/event-board/common/api/server.js new file mode 100644 index 00000000..8f7828f3 --- /dev/null +++ b/examples/event-board/common/api/server.js @@ -0,0 +1,20 @@ +import { serve } from '@hono/node-server' +import { Hono } from 'hono' +import { compress } from 'hono/compress' +import { api } from './index.js' + +const DEFAULT_API_PORT = 3001 +const API_PORT = Number(process.env.PORT || DEFAULT_API_PORT) +const app = new Hono() + +app.use(compress()) +app.use('/api/*', api()) + +app.get('/', c => c.redirect('http://localhost:5173')) + +serve({ + fetch: app.fetch, + port: API_PORT +}, (info) => { + console.info(`event board api started at http://localhost:${info.port}`) +}) diff --git a/examples/event-board/common/eslint.config.js b/examples/event-board/common/eslint.config.js new file mode 100644 index 00000000..bfc80249 --- /dev/null +++ b/examples/event-board/common/eslint.config.js @@ -0,0 +1,28 @@ +import bundlerConfig from '@trigen/eslint-config/bundler' +import moduleConfig from '@trigen/eslint-config/module' +import tsConfig from '@trigen/eslint-config/typescript' +import env from '@trigen/eslint-config/env' +import rootConfig from '../../../eslint.config.js' + +export default [ + ...rootConfig, + ...bundlerConfig, + ...tsConfig, + env.node, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } + }, + rules: { + '@typescript-eslint/no-magic-numbers': 'off', + 'import/no-default-export': 'off' + } + }, + ...moduleConfig.map(config => ({ + files: ['**/*.js'], + ...config + })) +] diff --git a/examples/event-board/common/package.json b/examples/event-board/common/package.json new file mode 100644 index 00000000..65efc9df --- /dev/null +++ b/examples/event-board/common/package.json @@ -0,0 +1,19 @@ +{ + "name": "event-board-common", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "sync": "node ../../../scripts/examples-sync.js . .." + }, + "dependencies": { + "@hono/node-server": "^1.19.9", + "@nano_kit/query": "*", + "@nano_kit/router": "*", + "@nano_kit/store": "*", + "hono": "^4.12.1" + }, + "devDependencies": { + "typescript": "~5.7.2" + } +} diff --git a/examples/event-board/common/src/routes.ts b/examples/event-board/common/src/routes.ts new file mode 100644 index 00000000..3ad3eb2e --- /dev/null +++ b/examples/event-board/common/src/routes.ts @@ -0,0 +1,7 @@ +import { routes } from './stores-di/router' + +declare module '@nano_kit/router' { + interface AppContext { + routes: typeof routes + } +} diff --git a/examples/event-board/common/src/services/events.ts b/examples/event-board/common/src/services/events.ts new file mode 100644 index 00000000..0b5aebfd --- /dev/null +++ b/examples/event-board/common/src/services/events.ts @@ -0,0 +1,107 @@ +import type { + BoardEvent, + EventsFilter, + EventsPage, + NewEventForm +} from './events.types' + +export * from './events.types' + +/** + * Absolute API origin used by the server renderer. + * Browser requests stay relative to the current origin. + */ +const API_ORIGIN = import.meta.env.SSR + ? import.meta.env.VITE_EVENT_BOARD_API_ORIGIN || (import.meta.env.DEV ? 'http://localhost:5173' : 'http://localhost:3001') + : '' +const HTTP_NOT_FOUND = 404 + +/** + * Fetch a cursor-paginated page of events. + * @param filter - Search, category, cursor, and limit filters. + * @returns Events page with optional next cursor. + */ +export async function fetchEvents(filter: EventsFilter = {}) { + const params = new URLSearchParams() + + if (filter.q) { + params.set('q', filter.q) + } + + if (filter.category) { + params.set('category', filter.category) + } + + if (filter.cursor) { + params.set('cursor', String(filter.cursor)) + } + + if (filter.limit) { + params.set('limit', String(filter.limit)) + } + + const response = await fetch(`${API_ORIGIN}/api/events?${params}`) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as EventsPage +} + +/** + * Fetch a single event by slug. + * @param slug - Event slug from the route. + * @returns Event details, or `null` when the event does not exist. + */ +export async function fetchEvent(slug: string) { + const response = await fetch(`${API_ORIGIN}/api/events/${slug}`) + + if (response.status === HTTP_NOT_FOUND) { + return null + } + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent +} + +/** + * Create a new event in the mock API. + * @param event - New event form payload. + * @returns Created event with generated id, slug, and attendees count. + */ +export async function createEvent(event: NewEventForm) { + const response = await fetch(`${API_ORIGIN}/api/events`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(event) + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent +} + +/** + * Mark the current user as going to an event. + * @param id - Event id. + * @returns Updated event with incremented attendees count. + */ +export async function rsvpEvent(id: string) { + const response = await fetch(`${API_ORIGIN}/api/events/${id}/rsvp`, { + method: 'POST' + }) + + if (!response.ok) { + throw new Error(response.statusText || `Request failed with status ${response.status}`) + } + + return await response.json() as BoardEvent +} diff --git a/examples/event-board/common/src/services/events.types.ts b/examples/event-board/common/src/services/events.types.ts new file mode 100644 index 00000000..ce3f3fa3 --- /dev/null +++ b/examples/event-board/common/src/services/events.types.ts @@ -0,0 +1,39 @@ +export const eventCategories = [ + 'conference', + 'meetup', + 'workshop', + 'webinar' +] as const + +export type EventCategory = typeof eventCategories[number] + +export interface BoardEvent { + id: string + slug: string + title: string + description: string + startsAt: number + location: string + category: EventCategory + attendees: number +} + +export interface EventsPage { + events: BoardEvent[] + nextCursor?: number +} + +export interface EventsFilter { + q?: string + category?: EventCategory | null + cursor?: number + limit?: number +} + +export interface NewEventForm { + title: string + description: string + startsAt: number | null + location: string + category: EventCategory +} diff --git a/examples/event-board/common/src/stores-di/events.ts b/examples/event-board/common/src/stores-di/events.ts new file mode 100644 index 00000000..bf41e7f6 --- /dev/null +++ b/examples/event-board/common/src/stores-di/events.ts @@ -0,0 +1,256 @@ +import { + action, + computed, + debounce, + inject, + not, + pace, + signal +} from '@nano_kit/store' +import { Navigation$ } from '@nano_kit/router' +import { + type InfinitePages, + disabled, + entities, + entity, + onError, + onSuccess, + queryKey +} from '@nano_kit/query' +import { + type BoardEvent, + type EventCategory, + type EventsPage, + type NewEventForm, + createEvent, + fetchEvent, + fetchEvents, + rsvpEvent +} from '#src/services/events' +import { Params$ } from './router' +import { Client$ } from './query' +import { datetimeLocalValue } from './utils' + +const SEARCH_DEBOUNCE = 600 +const MOCK_EVENT_START_OFFSET = 14 * 24 * 60 * 60 * 1000 + +export interface EventsList extends InfinitePages { + q?: string + category?: EventCategory | null +} + +export const EventsKey = queryKey<[q: string, category: EventCategory | null], EventsList>('events') + +export const EventKey = queryKey<[slug: string | undefined], BoardEvent | null>('event') + +export const EventEntity = entity('event') + +export function EventsList$() { + const { + $data, + infinite + } = inject(Client$) + const { + $q, + $category + } = inject(Params$) + const $pacedQ = computed(pace($q, debounce(SEARCH_DEBOUNCE))) + const [ + fetchNext, + $events, + $eventsError, + $eventsLoading + ] = infinite( + EventsKey, + [$pacedQ, $category], + lastPage => lastPage.nextCursor, + (q, category, cursor, ctx) => { + onSuccess(ctx, () => { + $data(ctx, data => data && { + ...data, + q, + category + }) + }) + + return fetchEvents({ + cursor, + q, + category + }) + }, + [ + entities(data => ({ + ...data, + pages: data.pages.map(page => ({ + ...page, + events: page.events.map(EventEntity) + })) + })) + ] + ) + + return { + fetchNext, + $events, + $eventsError, + $eventsLoading + } +} + +export function EventDetails$() { + const { query } = inject(Client$) + const { $slug } = inject(Params$) + const [ + $event, + $eventError, + $eventLoading + ] = query( + EventKey, + [$slug], + async slug => ( + slug + ? await fetchEvent(slug) + : null + ), + [entities(EventEntity)] + ) + + return { + $event, + $eventError, + $eventLoading + } +} + +export function NewEventForm$() { + const { + mutation, + revalidate + } = inject(Client$) + const navigation = inject(Navigation$) + const $title = signal('') + const $description = signal('') + const $startsAt = signal('') + const $location = signal('') + const $category = signal('meetup') + const $errors = computed(() => { + const errors: Partial> = {} + + if (!$title().trim()) { + errors.title = 'Title is required' + } + + if (!$description().trim()) { + errors.description = 'Description is required' + } + + if (!$startsAt()) { + errors.startsAt = 'Date and time are required' + } + + if (!$location().trim()) { + errors.location = 'Location is required' + } + + return errors + }) + const $valid = computed(() => Object.keys($errors()).length === 0) + const $payload = computed(() => ({ + title: $title().trim(), + description: $description().trim(), + startsAt: new Date($startsAt()).getTime(), + location: $location().trim(), + category: $category() + })) + const reset = action(() => { + $title('') + $description('') + $startsAt('') + $location('') + $category('meetup') + }) + const fillMock = action(() => { + $title('Frontend Architecture Night') + $description('Short talks about SSR, routing, query caching, and pragmatic app architecture.') + $startsAt(datetimeLocalValue(Date.now() + MOCK_EVENT_START_OFFSET)) + $location('Online') + $category('meetup') + }) + const [ + submit, + $createdEvent, + $createError, + $createLoading + ] = mutation<[], BoardEvent>( + (ctx) => { + const payload = $payload() + + onSuccess(ctx, (created) => { + revalidate(EventsKey) + reset() + navigation.push(`/events/${created.slug}`) + }) + + return createEvent(payload) + }, + [ + disabled(not($valid)), + entities(EventEntity) + ] + ) + + return { + $title, + $description, + $startsAt, + $location, + $category, + $errors, + $valid, + $createdEvent, + $createError, + $createLoading, + fillMock, + submit, + reset + } +} + +export function RsvpEvent$() { + const { + $data, + mutation + } = inject(Client$) + const [ + rsvp, + $rsvpEvent, + $rsvpError, + $rsvpLoading + ] = mutation<[id: string], BoardEvent>( + (id, ctx) => { + const key = EventEntity(id) + const previousEvent = $data(key) + + if (previousEvent) { + $data(key, { + ...previousEvent, + attendees: previousEvent.attendees + 1 + }) + + onError(ctx, () => { + $data(key, previousEvent) + }) + } + + return rsvpEvent(id) + } + ) + + return { + rsvp, + $rsvpEvent, + $rsvpError, + $rsvpLoading + } +} diff --git a/examples/event-board/common/src/stores-di/query.ts b/examples/event-board/common/src/stores-di/query.ts new file mode 100644 index 00000000..929ea8f3 --- /dev/null +++ b/examples/event-board/common/src/stores-di/query.ts @@ -0,0 +1,16 @@ +import { + client, + dedupeTime, + infinites, + mutations, + ssr +} from '@nano_kit/query' + +export function Client$() { + return client( + dedupeTime(5 * 60 * 1000), + infinites(), + mutations(), + ssr() + ) +} diff --git a/examples/event-board/common/src/stores-di/router.ts b/examples/event-board/common/src/stores-di/router.ts new file mode 100644 index 00000000..a5cb81cf --- /dev/null +++ b/examples/event-board/common/src/stores-di/router.ts @@ -0,0 +1,36 @@ +import { inject } from '@nano_kit/store' +import { + Location$, + param, + searchParam, + searchParams +} from '@nano_kit/router' +import { + type EventCategory, + eventCategories +} from '#src/services/events' + +export const routes = { + home: '/', + newEvent: '/events/new', + event: '/events/:slug' +} as const + +export function Params$() { + const $location = inject(Location$) + const $searchParams = searchParams($location) + const $slug = param($location, 'slug') + const $q = searchParam($searchParams, 'q', value => value || '') + const $category = searchParam($searchParams, 'category', (value): EventCategory | null => ( + eventCategories.includes(value as EventCategory) + ? value as EventCategory + : null + )) + + return { + $slug, + $q, + $category, + $searchParams + } +} diff --git a/examples/event-board/common/src/stores-di/utils.ts b/examples/event-board/common/src/stores-di/utils.ts new file mode 100644 index 00000000..25d8aef9 --- /dev/null +++ b/examples/event-board/common/src/stores-di/utils.ts @@ -0,0 +1,3 @@ +export function datetimeLocalValue(value: number) { + return new Date(value).toISOString().slice(0, 16) +} diff --git a/examples/event-board/common/tsconfig.json b/examples/event-board/common/tsconfig.json new file mode 100644 index 00000000..ea982ab2 --- /dev/null +++ b/examples/event-board/common/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "allowJs": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "paths": { + "#src/*": ["./src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts", "*.js"] +} diff --git a/examples/rick-and-morty/common/eslint.config.js b/examples/rick-and-morty/common/eslint.config.js new file mode 100644 index 00000000..152dcf29 --- /dev/null +++ b/examples/rick-and-morty/common/eslint.config.js @@ -0,0 +1,22 @@ +import bundlerConfig from '@trigen/eslint-config/bundler' +import tsConfig from '@trigen/eslint-config/typescript' +import env from '@trigen/eslint-config/env' +import rootConfig from '../../../eslint.config.js' + +export default [ + ...rootConfig, + ...bundlerConfig, + ...tsConfig, + env.node, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } + }, + rules: { + 'import/no-default-export': 'off' + } + } +] diff --git a/examples/rick-and-morty/common/package.json b/examples/rick-and-morty/common/package.json new file mode 100644 index 00000000..8de8902f --- /dev/null +++ b/examples/rick-and-morty/common/package.json @@ -0,0 +1,20 @@ +{ + "name": "rick-and-morty-common", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "sync": "node ../../../scripts/examples-sync.js . .." + }, + "dependencies": { + "@nano_kit/query": "*", + "@nano_kit/react": "*", + "@nano_kit/react-router": "*", + "@nano_kit/router": "*", + "@nano_kit/react-ssr": "*", + "@nano_kit/store": "*" + }, + "devDependencies": { + "typescript": "~5.7.2" + } +} diff --git a/examples/rick-and-morty/common/src/common/constants.ts b/examples/rick-and-morty/common/src/common/constants.ts new file mode 100644 index 00000000..8af7608c --- /dev/null +++ b/examples/rick-and-morty/common/src/common/constants.ts @@ -0,0 +1,2 @@ +export const OK_STATUS = 200 +export const STALE_TIME = 3000_000 // 5 minutes diff --git a/examples/rick-and-morty/common/src/routes.ts b/examples/rick-and-morty/common/src/routes.ts new file mode 100644 index 00000000..3ad3eb2e --- /dev/null +++ b/examples/rick-and-morty/common/src/routes.ts @@ -0,0 +1,7 @@ +import { routes } from './stores-di/router' + +declare module '@nano_kit/router' { + interface AppContext { + routes: typeof routes + } +} diff --git a/examples/rick-and-morty/common/src/services/api.ts b/examples/rick-and-morty/common/src/services/api.ts new file mode 100644 index 00000000..7a238370 --- /dev/null +++ b/examples/rick-and-morty/common/src/services/api.ts @@ -0,0 +1,78 @@ +import type { + Character, + Episode, + Location, + ApiResponse, + Info, + CharacterFilter, + LocationFilter, + EpisodeFilter +} from './types' + +export * from './types' + +const BASE_URL = 'https://trigensoftware.github.io/rick-and-morty-api/api' + +async function fetchApi(url: string): Promise> { + try { + const response = await fetch(url) + + if (!response.ok) { + return { + status: response.status, + statusMessage: response.statusText || String(response.status), + data: {} as T + } + } + + const data = await response.json() + + return { + status: response.status, + statusMessage: response.statusText || String(response.status), + data + } + } catch (error) { + return { + status: 500, + statusMessage: error instanceof Error ? error.message : 'Unknown error', + data: {} as T + } + } +} + +export async function getCharacters(filters?: CharacterFilter): Promise>> { + return await fetchApi(`${BASE_URL}/character/page/${filters?.page || 1}.json`) +} + +export async function getCharacter(id: number): Promise> { + return await fetchApi(`${BASE_URL}/character/${id}.json`) +} + +export async function getLocationResidents(id: number): Promise> { + return await fetchApi(`${BASE_URL}/location/residents/${id}.json`) +} + +export async function getEpisodeCharacters(id: number): Promise> { + return await fetchApi(`${BASE_URL}/episode/characters/${id}.json`) +} + +export async function getLocations(filters?: LocationFilter): Promise>> { + return await fetchApi(`${BASE_URL}/location/page/${filters?.page || 1}.json`) +} + +export async function getLocation(id: number): Promise> { + return await fetchApi(`${BASE_URL}/location/${id}.json`) +} + +export async function getEpisodes(filters?: EpisodeFilter): Promise>> { + return await fetchApi(`${BASE_URL}/episode/page/${filters?.page || 1}.json`) +} + +export async function getEpisode(id: number): Promise> { + return await fetchApi(`${BASE_URL}/episode/${id}.json`) +} + +export async function getCharacterEpisodes(id: number): Promise> { + return await fetchApi(`${BASE_URL}/character/episodes/${id}.json`) +} diff --git a/examples/rick-and-morty/common/src/services/types.ts b/examples/rick-and-morty/common/src/services/types.ts new file mode 100644 index 00000000..7e54fdf8 --- /dev/null +++ b/examples/rick-and-morty/common/src/services/types.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +export interface CharacterLocation { + name: string + url: string +} + +export interface ResourceBase { + id: number + name: string + url: string + created: string +} + +export interface CharacterFilter { + page?: number +} + +export interface LocationFilter { + page?: number +} + +export interface EpisodeFilter { + page?: number +} + +export interface Character extends ResourceBase { + status: 'Dead' | 'Alive' | 'unknown' + species: string + type: string + gender: 'Female' | 'Male' | 'Genderless' | 'unknown' + origin: CharacterLocation + location: CharacterLocation + image: string + episode: string[] +} + +export interface Location extends ResourceBase { + type: string + dimension: string + residents: string[] +} + +export interface Episode extends ResourceBase { + air_date: string + episode: string + characters: string[] +} + +export interface ApiResponse { + status: number + statusMessage: string + data: T +} + +export interface Info { + info?: { + count: number + pages: number + next: string | null + prev: string | null + } + results?: T +} diff --git a/examples/rick-and-morty/common/src/stores-di/characters.ts b/examples/rick-and-morty/common/src/stores-di/characters.ts new file mode 100644 index 00000000..e6b796ff --- /dev/null +++ b/examples/rick-and-morty/common/src/stores-di/characters.ts @@ -0,0 +1,127 @@ +import { inject } from '@nano_kit/store' +import { queryKey } from '@nano_kit/query' +import { + type Character, + getEpisodeCharacters, + getCharacter, + getCharacters, + getLocationResidents +} from '../services/api' +import { OK_STATUS } from '../common/constants' +import { + type Page, + Client$ +} from './query' +import { Params$ } from './router' + +export function Characters$() { + const { query } = inject(Client$) + const { $charactersPage } = inject(Params$) + const [ + $characters, + $charactersError, + $charactersLoading + ] = query<[page: number], Page>( + queryKey('characters'), + [$charactersPage], + async (page) => { + if (page === 0) { + return { + items: [], + totalPages: 0 + } + } + + const response = await getCharacters({ + page + }) + + if (response.status === OK_STATUS && response.data.results && response.data.info) { + return { + items: response.data.results, + totalPages: response.data.info.pages + } + } + + throw new Error(response.statusMessage) + } + ) + + return { + $characters, + $charactersError, + $charactersLoading + } +} + +export function Character$() { + const { query } = inject(Client$) + const { $characterId } = inject(Params$) + const [ + $character, + $characterError, + $characterLoading + ] = query<[id: number | null], Character | null>( + queryKey('character'), + [$characterId], + async (id) => { + if (id === null) { + return null + } + + const response = await getCharacter(id) + + if (response.status === OK_STATUS && response.data) { + return response.data + } + + throw new Error(response.statusMessage) + } + ) + + return { + $character, + $characterError, + $characterLoading + } +} + +export function Residents$() { + const { query } = inject(Client$) + const { + $locationId, + $episodeId + } = inject(Params$) + const [ + $residents, + $residentsError, + $residentsLoading + ] = query<[locationId: number | null, episodeId: number | null], Character[]>( + queryKey('residents'), + [ + $locationId, + $episodeId + ], + async (locationId, episodeId) => { + if (!locationId && !episodeId) { + return [] + } + + const response = locationId + ? await getLocationResidents(locationId) + : await getEpisodeCharacters(episodeId as number) + + if (response.status === OK_STATUS) { + return response.data + } + + throw new Error(response.statusMessage) + } + ) + + return { + $residents, + $residentsError, + $residentsLoading + } +} diff --git a/examples/rick-and-morty/common/src/stores-di/episodes.ts b/examples/rick-and-morty/common/src/stores-di/episodes.ts new file mode 100644 index 00000000..56aeb1c4 --- /dev/null +++ b/examples/rick-and-morty/common/src/stores-di/episodes.ts @@ -0,0 +1,118 @@ +import { inject } from '@nano_kit/store' +import { queryKey } from '@nano_kit/query' +import { + type Episode, + getCharacterEpisodes, + getEpisode, + getEpisodes +} from '../services/api' +import { OK_STATUS } from '../common/constants' +import { + type Page, + Client$ +} from './query' +import { Params$ } from './router' + +export function Episodes$() { + const { query } = inject(Client$) + const { $episodesPage } = inject(Params$) + const [ + $episodes, + $episodesError, + $episodesLoading + ] = query<[page: number], Page>( + queryKey('episodes'), + [$episodesPage], + async (page) => { + if (page === 0) { + return { + items: [], + totalPages: 0 + } + } + + const response = await getEpisodes({ + page + }) + + if (response.status === OK_STATUS && response.data.results && response.data.info) { + return { + items: response.data.results, + totalPages: response.data.info.pages + } + } + + throw new Error(response.statusMessage) + } + ) + + return { + $episodes, + $episodesError, + $episodesLoading + } +} + +export function Episode$() { + const { query } = inject(Client$) + const { $episodeId } = inject(Params$) + const [ + $episode, + $episodeError, + $episodeLoading + ] = query<[id: number | null], Episode | null>( + queryKey('episode'), + [$episodeId], + async (id) => { + if (id === null) { + return null + } + + const response = await getEpisode(id) + + if (response.status === OK_STATUS && response.data) { + return response.data + } + + throw new Error(response.statusMessage) + } + ) + + return { + $episode, + $episodeError, + $episodeLoading + } +} + +export function CharacterEpisodes$() { + const { query } = inject(Client$) + const { $characterId } = inject(Params$) + const [ + $characterEpisodes, + $characterEpisodesError, + $characterEpisodesLoading + ] = query<[characterId: number | null], Episode[]>( + queryKey('characterEpisodes'), + [$characterId], + async (characterId) => { + if (!characterId) { + return [] + } + + const response = await getCharacterEpisodes(characterId) + + if (response.status === OK_STATUS) { + return response.data + } + + throw new Error(response.statusMessage) + } + ) + + return { + $characterEpisodes, + $characterEpisodesError, + $characterEpisodesLoading + } +} diff --git a/examples/rick-and-morty/common/src/stores-di/locations.ts b/examples/rick-and-morty/common/src/stores-di/locations.ts new file mode 100644 index 00000000..90686b2d --- /dev/null +++ b/examples/rick-and-morty/common/src/stores-di/locations.ts @@ -0,0 +1,85 @@ +import { inject } from '@nano_kit/store' +import { queryKey } from '@nano_kit/query' +import { + type Location, + getLocation, + getLocations +} from '../services/api' +import { OK_STATUS } from '../common/constants' +import { + type Page, + Client$ +} from './query' +import { Params$ } from './router' + +export function Locations$() { + const { query } = inject(Client$) + const { $locationsPage } = inject(Params$) + const [ + $locations, + $locationsError, + $locationsLoading + ] = query<[page: number], Page>( + queryKey('locations'), + [$locationsPage], + async (page) => { + if (page === 0) { + return { + items: [], + totalPages: 0 + } + } + + const response = await getLocations({ + page + }) + + if (response.status === OK_STATUS && response.data.results && response.data.info) { + return { + items: response.data.results, + totalPages: response.data.info.pages + } + } + + throw new Error(response.statusMessage) + } + ) + + return { + $locations, + $locationsError, + $locationsLoading + } +} + +export function Location$() { + const { query } = inject(Client$) + const { $locationId } = inject(Params$) + const [ + $location, + $locationError, + $locationLoading + ] = query<[id: number | null], Location | null>( + queryKey('location'), + [$locationId], + async (id) => { + if (id === null) { + return null + } + + const response = await getLocation(id) + + if (response.status === OK_STATUS && response.data) { + return response.data + } + + throw new Error(response.statusMessage) + } + ) + + return { + $location, + $locationError, + $locationLoading + } +} diff --git a/examples/rick-and-morty/common/src/stores-di/memoryStorage.ts b/examples/rick-and-morty/common/src/stores-di/memoryStorage.ts new file mode 100644 index 00000000..24429567 --- /dev/null +++ b/examples/rick-and-morty/common/src/stores-di/memoryStorage.ts @@ -0,0 +1,82 @@ +import type { + CacheKey, + CacheShardKey, + CacheEntry, + Storage +} from '@nano_kit/query' + +interface StoredEntry { + data: CacheEntry + expires: number +} + +/** + * In-memory adapter using a Map of Maps. + * @returns In-memory storage implementation. + */ +/* @__NO_SIDE_EFFECTS__ */ +export function memoryStorage(): Storage { + const shards = new Map>() + + return { + get(key: CacheKey) { + const shard = shards.get(key.shard) + + if (!shard) { + return Promise.resolve(null) + } + + const entry = shard.get(key.key) + + if (!entry) { + return Promise.resolve(null) + } + + const now = Date.now() + + if (entry.expires < now || entry.data.expires < now) { + shard.delete(key.key) + + if (!shard.size) { + shards.delete(key.shard) + } + + return Promise.resolve(null) + } + + return Promise.resolve(entry.data) + }, + set(cacheKey: CacheKey, entry: CacheEntry, lifetime: number) { + let shard = shards.get(cacheKey.shard) + + if (!shard) { + shard = new Map() + shards.set(cacheKey.shard, shard) + } + + shard.set(cacheKey.key, { + data: entry, + expires: Date.now() + lifetime + }) + + return Promise.resolve() + }, + delete(cacheKey: CacheShardKey | CacheKey) { + if (cacheKey.key === undefined) { + shards.delete(cacheKey.shard) + } else { + const shard = shards.get(cacheKey.shard) + + if (shard) { + shard.delete(cacheKey.key) + + if (!shard.size) { + shards.delete(cacheKey.shard) + } + } + } + + return Promise.resolve() + } + } +} diff --git a/examples/rick-and-morty/common/src/stores-di/query.ts b/examples/rick-and-morty/common/src/stores-di/query.ts new file mode 100644 index 00000000..99020ac8 --- /dev/null +++ b/examples/rick-and-morty/common/src/stores-di/query.ts @@ -0,0 +1,19 @@ +import { + client, + dedupeTime, + ssr +} from '@nano_kit/query' + +export interface Page { + items: T[] + totalPages: number +} + +const DEDUPE_TIME = 3000_000 // 5 minutes + +export function Client$() { + return client( + dedupeTime(DEDUPE_TIME), + ssr() + ) +} diff --git a/examples/rick-and-morty/common/src/stores-di/router.ts b/examples/rick-and-morty/common/src/stores-di/router.ts new file mode 100644 index 00000000..875c52eb --- /dev/null +++ b/examples/rick-and-morty/common/src/stores-di/router.ts @@ -0,0 +1,40 @@ +import { inject } from '@nano_kit/store' +import { + forRoute, + Location$, + param, + searchParam, + searchParams +} from '@nano_kit/router' + +export const routes = { + home: '/', + characters: '/characters', + character: '/character/:id', + locations: '/locations', + location: '/location/:id', + episodes: '/episodes', + episode: '/episode/:id' +} as const + +export function Params$() { + const $location = inject(Location$) + const $searchParams = searchParams($location) + const $id = param($location, 'id', v => (v ? Number(v) : null)) + const $page = searchParam($searchParams, 'page', v => (v ? Number(v) : 1)) + const $characterId = forRoute($location, 'character', $id, null) + const $locationId = forRoute($location, 'location', $id, null) + const $episodeId = forRoute($location, 'episode', $id, null) + const $charactersPage = forRoute($location, 'characters', $page, 1) + const $locationsPage = forRoute($location, 'locations', $page, 1) + const $episodesPage = forRoute($location, 'episodes', $page, 1) + + return { + $charactersPage, + $locationsPage, + $episodesPage, + $characterId, + $locationId, + $episodeId + } +} diff --git a/examples/rick-and-morty/common/src/stores/characters.ts b/examples/rick-and-morty/common/src/stores/characters.ts new file mode 100644 index 00000000..5b7bafdc --- /dev/null +++ b/examples/rick-and-morty/common/src/stores/characters.ts @@ -0,0 +1,98 @@ +import { queryKey } from '@nano_kit/query' +import { + type Character, + getEpisodeCharacters, + getCharacter, + getCharacters, + getLocationResidents +} from '../services/api' +import { OK_STATUS } from '../common/constants' +import { + type Page, + query +} from './query' +import { + $characterId, + $charactersPage, + $episodeId, + $locationId +} from './router' + +export const [ + $characters, + $charactersError, + $charactersLoading +] = query<[page: number], Page>( + queryKey('characters'), + [$charactersPage], + async (page) => { + if (page === 0) { + return { + items: [], + totalPages: 0 + } + } + + const response = await getCharacters({ + page + }) + + if (response.status === OK_STATUS && response.data.results && response.data.info) { + return { + items: response.data.results, + totalPages: response.data.info.pages + } + } + + throw new Error(response.statusMessage) + } +) + +export const [ + $character, + $characterError, + $characterLoading +] = query<[id: number | null], Character | null>( + queryKey('character'), + [$characterId], + async (id) => { + if (id === null) { + return null + } + + const response = await getCharacter(id) + + if (response.status === OK_STATUS && response.data) { + return response.data + } + + throw new Error(response.statusMessage) + } +) + +export const [ + $residents, + $residentsError, + $residentsLoading +] = query<[locationId: number | null, episodeId: number | null], Character[]>( + queryKey('residents'), + [ + $locationId, + $episodeId + ], + async (locationId, episodeId) => { + if (!locationId && !episodeId) { + return [] + } + + const response = locationId + ? await getLocationResidents(locationId) + : await getEpisodeCharacters(episodeId as number) + + if (response.status === OK_STATUS) { + return response.data + } + + throw new Error(response.statusMessage) + } +) diff --git a/examples/rick-and-morty/common/src/stores/episodes.ts b/examples/rick-and-morty/common/src/stores/episodes.ts new file mode 100644 index 00000000..636f63d3 --- /dev/null +++ b/examples/rick-and-morty/common/src/stores/episodes.ts @@ -0,0 +1,91 @@ +import { queryKey } from '@nano_kit/query' +import { + type Episode, + getCharacterEpisodes, + getEpisode, + getEpisodes +} from '../services/api' +import { OK_STATUS } from '../common/constants' +import { + type Page, + query +} from './query' +import { + $characterId, + $episodeId, + $episodesPage +} from './router' + +export const [ + $episodes, + $episodesError, + $episodesLoading +] = query<[page: number], Page>( + queryKey('episodes'), + [$episodesPage], + async (page) => { + if (page === 0) { + return { + items: [], + totalPages: 0 + } + } + + const response = await getEpisodes({ + page + }) + + if (response.status === OK_STATUS && response.data.results && response.data.info) { + return { + items: response.data.results, + totalPages: response.data.info.pages + } + } + + throw new Error(response.statusMessage) + } +) + +export const [ + $episode, + $episodeError, + $episodeLoading +] = query<[id: number | null], Episode | null>( + queryKey('episode'), + [$episodeId], + async (id) => { + if (id === null) { + return null + } + + const response = await getEpisode(id) + + if (response.status === OK_STATUS && response.data) { + return response.data + } + + throw new Error(response.statusMessage) + } +) + +export const [ + $characterEpisodes, + $characterEpisodesError, + $characterEpisodesLoading +] = query<[characterId: number | null], Episode[]>( + queryKey('characterEpisodes'), + [$characterId], + async (characterId) => { + if (!characterId) { + return [] + } + + const response = await getCharacterEpisodes(characterId) + + if (response.status === OK_STATUS) { + return response.data + } + + throw new Error(response.statusMessage) + } +) diff --git a/examples/rick-and-morty/common/src/stores/locations.ts b/examples/rick-and-morty/common/src/stores/locations.ts new file mode 100644 index 00000000..f8d1214e --- /dev/null +++ b/examples/rick-and-morty/common/src/stores/locations.ts @@ -0,0 +1,67 @@ +import { queryKey } from '@nano_kit/query' +import { + type Location, + getLocation, + getLocations +} from '../services/api' +import { OK_STATUS } from '../common/constants' +import { + type Page, + query +} from './query' +import { + $locationId, + $locationsPage +} from './router' + +export const [ + $locations, + $locationsError, + $locationsLoading +] = query<[page: number], Page>( + queryKey('locations'), + [$locationsPage], + async (page) => { + if (page === 0) { + return { + items: [], + totalPages: 0 + } + } + + const response = await getLocations({ + page + }) + + if (response.status === OK_STATUS && response.data.results && response.data.info) { + return { + items: response.data.results, + totalPages: response.data.info.pages + } + } + + throw new Error(response.statusMessage) + } +) + +export const [ + $location, + $locationError, + $locationLoading +] = query<[id: number | null], Location | null>( + queryKey('location'), + [$locationId], + async (id) => { + if (id === null) { + return null + } + + const response = await getLocation(id) + + if (response.status === OK_STATUS && response.data) { + return response.data + } + + throw new Error(response.statusMessage) + } +) diff --git a/examples/rick-and-morty/common/src/stores/query.ts b/examples/rick-and-morty/common/src/stores/query.ts new file mode 100644 index 00000000..f707ef4c --- /dev/null +++ b/examples/rick-and-morty/common/src/stores/query.ts @@ -0,0 +1,15 @@ +import { + client, + dedupeTime +} from '@nano_kit/query' + +export interface Page { + items: T[] + totalPages: number +} + +const DEDUPE_TIME = 3000_000 // 5 minutes + +export const { query } = client( + dedupeTime(DEDUPE_TIME) +) diff --git a/examples/rick-and-morty/common/src/stores/router.ts b/examples/rick-and-morty/common/src/stores/router.ts new file mode 100644 index 00000000..e065d5b1 --- /dev/null +++ b/examples/rick-and-morty/common/src/stores/router.ts @@ -0,0 +1,34 @@ +import { + browserNavigation, + buildPaths, + forRoute, + param, + searchParam, + searchParams +} from '@nano_kit/router' + +const routes = { + home: '/', + characters: '/characters', + character: '/character/:id', + locations: '/locations', + location: '/location/:id', + episodes: '/episodes', + episode: '/episode/:id' +} as const + +export const [$location, navigation] = browserNavigation(routes) + +const $searchParams = searchParams($location) +const $id = param($location, 'id', v => (v ? Number(v) : null)) +const $page = searchParam($searchParams, 'page', v => (v ? Number(v) : 1)) + +export const $characterId = forRoute($location, 'character', $id, null) +export const $locationId = forRoute($location, 'location', $id, null) +export const $episodeId = forRoute($location, 'episode', $id, null) + +export const $charactersPage = forRoute($location, 'characters', $page, 1) +export const $locationsPage = forRoute($location, 'locations', $page, 1) +export const $episodesPage = forRoute($location, 'episodes', $page, 1) + +export const paths = buildPaths(routes) diff --git a/examples/rick-and-morty/common/tsconfig.json b/examples/rick-and-morty/common/tsconfig.json new file mode 100644 index 00000000..ea982ab2 --- /dev/null +++ b/examples/rick-and-morty/common/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "allowJs": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "paths": { + "#src/*": ["./src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts", "*.js"] +} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/character/[characterId]/page.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/character/[characterId]/page.tsx index 7f90e26b..9e05a275 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/character/[characterId]/page.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/character/[characterId]/page.tsx @@ -1,7 +1,8 @@ import { Dehydration } from '@nano_kit/react' import { NextNavigation } from '@nano_kit/next-router' import { routes } from '@/stores/router' -import CharacterPage, { Stores$ } from '@/ui/pages/Character' +import CharacterPage from '@/ui/pages/Character' +import { Stores$ } from '@/ui/pages/Character.stores' export default function Page() { return ( diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/characters/page.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/characters/page.tsx index 9520e16d..8e22c096 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/characters/page.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/characters/page.tsx @@ -1,7 +1,8 @@ import { StaticDehydration } from '@nano_kit/react' import { NextNavigation } from '@nano_kit/next-router' import { routes } from '@/stores/router' -import CharactersPage, { Stores$ } from '@/ui/pages/Characters' +import CharactersPage from '@/ui/pages/Characters' +import { Stores$ } from '@/ui/pages/Characters.stores' export default function Page() { return ( diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/episode/[episodeId]/page.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/episode/[episodeId]/page.tsx index 0eab5f3c..9b10de35 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/episode/[episodeId]/page.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/episode/[episodeId]/page.tsx @@ -1,7 +1,8 @@ import { Dehydration } from '@nano_kit/react' import { NextNavigation } from '@nano_kit/next-router' import { routes } from '@/stores/router' -import EpisodePage, { Stores$ } from '@/ui/pages/Episode' +import EpisodePage from '@/ui/pages/Episode' +import { Stores$ } from '@/ui/pages/Episode.stores' export default function Page() { return ( diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/episodes/page.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/episodes/page.tsx index 55534c78..8079755c 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/episodes/page.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/episodes/page.tsx @@ -1,7 +1,8 @@ import { Dehydration } from '@nano_kit/react' import { NextNavigation } from '@nano_kit/next-router' import { routes } from '@/stores/router' -import EpisodesPage, { Stores$ } from '@/ui/pages/Episodes' +import EpisodesPage from '@/ui/pages/Episodes' +import { Stores$ } from '@/ui/pages/Episodes.stores' export default function Page() { return ( diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/globals.css b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/globals.css index 45213a36..36916a29 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/globals.css +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/globals.css @@ -24,3 +24,1573 @@ body { background-color: #fafafa; } } + +/* rick-and-morty css modules migrated start */ + +/* app/layout.css */ +/* DISCLAIMER! VIBECODED! */ +/* App - Material Design */ +.layout-app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.layout-main { + flex: 1; +} + + +/* ui/blocks/CharacterCard/CharacterCard.css */ +/* DISCLAIMER! VIBECODED! */ +/* Character Card - Material Design */ +.character-card-card { + background: #ffffff; + border-radius: 0.5rem; + overflow: hidden; + transition: box-shadow 0.2s ease; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); +} + +.character-card-card:hover { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); +} + +.character-card-link { + display: block; + text-decoration: none; + color: inherit; +} + +.character-card-image-wrapper { + position: relative; + aspect-ratio: 1; + overflow: hidden; + background: #f5f5f5; +} + +.character-card-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.character-card-content { + padding: 1rem; +} + +.character-card-name { + font-size: 1.125rem; + font-weight: 500; + color: #212121; + margin: 0 0 1rem 0; + line-height: 1.4; +} + +.character-card-info { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.character-card-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.character-card-label { + font-weight: 400; + color: #757575; + font-size: 0.875rem; +} + +.character-card-value { + font-weight: 500; + color: #424242; + font-size: 0.875rem; + text-align: right; +} + +/* Status indicators */ +.character-card-status { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; +} + +.character-card-status--alive { + background-color: #e8f5e8; + color: #2e7d32; +} + +.character-card-status--dead { + background-color: #ffebee; + color: #c62828; +} + +.character-card-status--unknown { + background-color: #f5f5f5; + color: #757575; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .character-card-card { + background: #424242; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24), 0 1px 2px rgba(0, 0, 0, 0.32); + } + + .character-card-card:hover { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.32), 0 3px 6px rgba(0, 0, 0, 0.32); + } + + .character-card-image-wrapper { + background: #616161; + } + + .character-card-name { + color: #ffffff; + } + + .character-card-label { + color: #bdbdbd; + } + + .character-card-value { + color: #e0e0e0; + } + + .character-card-status--alive { + background-color: #1b5e20; + color: #a5d6a7; + } + + .character-card-status--dead { + background-color: #b71c1c; + color: #ef9a9a; + } + + .character-card-status--unknown { + background-color: #616161; + color: #bdbdbd; + } +} + +/* Responsive design */ +@media (max-width: 768px) { + .character-card-content { + padding: 0.875rem; + } + + .character-card-name { + font-size: 1rem; + margin-bottom: 0.875rem; + } + + .character-card-row { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .character-card-value { + text-align: left; + font-size: 0.8125rem; + } + + .character-card-label { + font-size: 0.8125rem; + } +} + + +/* ui/blocks/CharacterDetail/CharacterDetail.css */ +/* DISCLAIMER! VIBECODED! */ +.character-detail-container { + width: 100%; +} + +/* Header */ +.character-detail-header { + display: flex; + gap: 2rem; + margin-bottom: 2rem; + align-items: flex-start; +} + +.character-detail-image { + width: 200px; + height: 200px; + border-radius: 12px; + object-fit: cover; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); +} + +.character-detail-info { + flex: 1; +} + +.character-detail-name { + font-size: 2rem; + font-weight: 500; + margin: 0 0 1rem 0; + color: #212121; +} + +.character-detail-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.1rem; + font-weight: 500; + margin-bottom: 0.5rem; + color: #424242; +} + +.character-detail-status-indicator { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.character-detail-alive { + background-color: #2e7d32; +} + +.character-detail-dead { + background-color: #c62828; +} + +.character-detail-unknown { + background-color: #757575; +} + +.character-detail-type, +.character-detail-gender { + margin: 0.5rem 0; + color: #757575; + font-size: 1rem; +} + +/* Details */ +.character-detail-details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.character-detail-section { + background: #ffffff; + border-radius: 12px; + padding: 1.5rem; + border: 1px solid #e0e0e0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); +} + +.character-detail-section h2 { + font-size: 1.25rem; + font-weight: 500; + margin: 0 0 1rem 0; + color: #212121; +} + +.character-detail-section p { + margin: 0; + color: #424242; + font-size: 1rem; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .character-detail-name { + color: #e0e0e0; + } + + .character-detail-status { + color: #bdbdbd; + } + + .character-detail-type, + .character-detail-gender { + color: #9e9e9e; + } + + .character-detail-section { + background: #424242; + border-color: #616161; + } + + .character-detail-section h2 { + color: #e0e0e0; + } + + .character-detail-section p { + color: #bdbdbd; + } +} + +/* Responsive design */ +@media (max-width: 768px) { + .character-detail-container { + padding: 1rem; + } + + .character-detail-header { + flex-direction: column; + align-items: center; + text-align: center; + } + + .character-detail-image { + width: 150px; + height: 150px; + } + + .character-detail-name { + font-size: 1.75rem; + } + + .character-detail-details { + grid-template-columns: 1fr; + gap: 1rem; + } + + .character-detail-section { + padding: 1rem; + } +} + + +/* ui/blocks/CharactersGrid/CharactersGrid.css */ +/* DISCLAIMER! VIBECODED! */ +/* Grid layout */ +.characters-grid-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +/* Responsive design */ +@media (max-width: 768px) { + .characters-grid-grid { + grid-template-columns: 1fr; + gap: 1rem; + } +} + +@media (max-width: 640px) { + .characters-grid-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + } +} + + +/* ui/blocks/EpisodeCard/EpisodeCard.css */ +/* DISCLAIMER! VIBECODED! */ +/* Episode Card - Material Design */ +.episode-card-card { + background: #ffffff; + border-radius: 0.5rem; + overflow: hidden; + transition: box-shadow 0.2s ease; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); +} + +.episode-card-card:hover { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); +} + +.episode-card-link { + display: block; + text-decoration: none; + color: inherit; +} + +.episode-card-content { + padding: 1rem; +} + +.episode-card-name { + font-size: 1.125rem; + font-weight: 500; + color: #212121; + margin: 0 0 1rem 0; + line-height: 1.4; +} + +.episode-card-info { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.episode-card-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.episode-card-label { + font-weight: 400; + color: #757575; + font-size: 0.875rem; +} + +.episode-card-value { + font-weight: 500; + color: #424242; + font-size: 0.875rem; + text-align: right; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .episode-card-card { + background: #424242; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24), 0 1px 2px rgba(0, 0, 0, 0.32); + } + + .episode-card-card:hover { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.32), 0 3px 6px rgba(0, 0, 0, 0.32); + } + + .episode-card-name { + color: #ffffff; + } + + .episode-card-label { + color: #bdbdbd; + } + + .episode-card-value { + color: #e0e0e0; + } +} + +/* Responsive design */ +@media (max-width: 768px) { + .episode-card-content { + padding: 0.875rem; + } + + .episode-card-name { + font-size: 1rem; + margin-bottom: 0.875rem; + } + + .episode-card-row { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .episode-card-value { + text-align: left; + font-size: 0.8125rem; + } + + .episode-card-label { + font-size: 0.8125rem; + } +} + + +/* ui/blocks/EpisodeDetail/EpisodeDetail.css */ +/* DISCLAIMER! VIBECODED! */ +.episode-detail-container { + width: 100%; +} + +/* Header */ +.episode-detail-header { + margin-bottom: 2rem; +} + +.episode-detail-info { + max-width: 600px; +} + +.episode-detail-name { + font-size: 2.5rem; + font-weight: 500; + margin: 0 0 1.5rem 0; + color: #212121; +} + +.episode-detail-episode, +.episode-detail-air-date { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + font-size: 1.125rem; +} + +.episode-detail-label { + font-weight: 500; + color: #757575; + min-width: 80px; +} + +.episode-detail-value { + font-weight: 500; + color: #424242; +} + +/* Details */ +.episode-detail-details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.episode-detail-section { + background: #ffffff; + border-radius: 12px; + padding: 1.5rem; + border: 1px solid #e0e0e0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); +} + +.episode-detail-section h2 { + font-size: 1.25rem; + font-weight: 500; + margin: 0 0 1rem 0; + color: #212121; +} + +.episode-detail-section p { + margin: 0; + color: #424242; + font-size: 1rem; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .episode-detail-name { + color: #e0e0e0; + } + + .episode-detail-label { + color: #9e9e9e; + } + + .episode-detail-value { + color: #bdbdbd; + } + + .episode-detail-section { + background: #424242; + border-color: #616161; + } + + .episode-detail-section h2 { + color: #e0e0e0; + } + + .episode-detail-section p { + color: #bdbdbd; + } +} + +/* Responsive design */ +@media (max-width: 768px) { + .episode-detail-container { + padding: 1rem; + } + + .episode-detail-name { + font-size: 2rem; + } + + .episode-detail-episode, + .episode-detail-air-date { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .episode-detail-label { + min-width: auto; + } + + .episode-detail-details { + grid-template-columns: 1fr; + gap: 1rem; + } + + .episode-detail-section { + padding: 1rem; + } +} + + +/* ui/blocks/EpisodesGrid/EpisodesGrid.css */ +/* DISCLAIMER! VIBECODED! */ +/* Grid layout */ +.episodes-grid-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +/* Responsive design */ +@media (max-width: 768px) { + .episodes-grid-grid { + grid-template-columns: 1fr; + gap: 1rem; + } +} + +@media (max-width: 640px) { + .episodes-grid-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + } +} + + +/* ui/blocks/Header/Header.css */ +/* DISCLAIMER! VIBECODED! */ +.header-container { + margin: 0 auto; + padding: 0 1rem; + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + height: 4rem; +} + +.header-header { + background: #ffffff; + border-bottom: 1px solid #e0e0e0; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); +} + +.header-title { + font-size: 1.5rem; + font-weight: 500; + color: #212121; + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; + justify-self: start; +} + +.header-logo { + font-size: 1.75rem; +} + +.header-nav { + display: flex; + gap: 0.5rem; + justify-self: center; +} + +.header-nav-link { + padding: 0.5rem 1rem; + color: #757575; + text-decoration: none; + font-weight: 500; + font-size: 0.875rem; + border-radius: 0.25rem; + transition: all 0.2s ease; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.header-nav-link:hover { + background-color: #f5f5f5; + color: #424242; +} + +.header-nav-link.active { + background-color: #1976d2; + color: white; +} + +.header-nav-link.active:hover { + background-color: #1565c0; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .header-header { + background: #424242; + border-bottom-color: #616161; + } + + .header-title { + color: #ffffff; + } + + .header-nav-link { + color: #bdbdbd; + } + + .header-nav-link:hover { + background-color: #616161; + color: #ffffff; + } + + .header-nav-link.active { + background-color: #1976d2; + color: white; + } + + .header-nav-link.active:hover { + background-color: #1565c0; + } +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .header-container { + display: flex; + flex-direction: column; + height: auto; + padding: 1rem; + gap: 1rem; + } + + .header-title { + font-size: 1.25rem; + } +} + + +/* ui/blocks/LocationCard/LocationCard.css */ +/* DISCLAIMER! VIBECODED! */ +/* Location Card - Material Design */ +.location-card-card { + background: #ffffff; + border-radius: 0.5rem; + overflow: hidden; + transition: box-shadow 0.2s ease; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); +} + +.location-card-card:hover { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); +} + +.location-card-link { + display: block; + text-decoration: none; + color: inherit; +} + +.location-card-content { + padding: 1rem; +} + +.location-card-name { + font-size: 1.125rem; + font-weight: 500; + color: #212121; + margin: 0 0 1rem 0; + line-height: 1.4; +} + +.location-card-info { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.location-card-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.location-card-label { + font-weight: 400; + color: #757575; + font-size: 0.875rem; +} + +.location-card-value { + font-weight: 500; + color: #424242; + font-size: 0.875rem; + text-align: right; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .location-card-card { + background: #424242; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24), 0 1px 2px rgba(0, 0, 0, 0.32); + } + + .location-card-card:hover { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.32), 0 3px 6px rgba(0, 0, 0, 0.32); + } + + .location-card-name { + color: #ffffff; + } + + .location-card-label { + color: #bdbdbd; + } + + .location-card-value { + color: #e0e0e0; + } +} + +/* Responsive design */ +@media (max-width: 768px) { + .location-card-content { + padding: 0.875rem; + } + + .location-card-name { + font-size: 1rem; + margin-bottom: 0.875rem; + } + + .location-card-row { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .location-card-value { + text-align: left; + font-size: 0.8125rem; + } + + .location-card-label { + font-size: 0.8125rem; + } +} + + +/* ui/blocks/LocationDetail/LocationDetail.css */ +/* DISCLAIMER! VIBECODED! */ +.location-detail-container { + width: 100%; +} + +/* Header */ +.location-detail-header { + margin-bottom: 2rem; +} + +.location-detail-info { + max-width: 600px; +} + +.location-detail-name { + font-size: 2.5rem; + font-weight: 500; + margin: 0 0 1.5rem 0; + color: #212121; +} + +.location-detail-type, +.location-detail-dimension { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + font-size: 1.125rem; +} + +.location-detail-label { + font-weight: 500; + color: #757575; + min-width: 80px; +} + +.location-detail-value { + font-weight: 500; + color: #424242; +} + +/* Details */ +.location-detail-details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.location-detail-section { + background: #ffffff; + border-radius: 12px; + padding: 1.5rem; + border: 1px solid #e0e0e0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); +} + +.location-detail-section h2 { + font-size: 1.25rem; + font-weight: 500; + margin: 0 0 1rem 0; + color: #212121; +} + +.location-detail-section p { + margin: 0; + color: #424242; + font-size: 1rem; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .location-detail-name { + color: #e0e0e0; + } + + .location-detail-label { + color: #9e9e9e; + } + + .location-detail-value { + color: #bdbdbd; + } + + .location-detail-section { + background: #424242; + border-color: #616161; + } + + .location-detail-section h2 { + color: #e0e0e0; + } + + .location-detail-section p { + color: #bdbdbd; + } +} + +/* Responsive design */ +@media (max-width: 768px) { + .location-detail-container { + padding: 1rem; + } + + .location-detail-name { + font-size: 2rem; + } + + .location-detail-type, + .location-detail-dimension { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .location-detail-label { + min-width: auto; + } + + .location-detail-details { + grid-template-columns: 1fr; + gap: 1rem; + } + + .location-detail-section { + padding: 1rem; + } +} + + +/* ui/blocks/LocationsGrid/LocationsGrid.css */ +/* DISCLAIMER! VIBECODED! */ +/* Grid layout */ +.locations-grid-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +/* Responsive design */ +@media (max-width: 768px) { + .locations-grid-grid { + grid-template-columns: 1fr; + gap: 1rem; + } +} + +@media (max-width: 640px) { + .locations-grid-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + } +} + + +/* ui/components/Pagination/Pagination.css */ +/* DISCLAIMER! VIBECODED! */ +/* Pagination - Material Design */ + +.pagination-pagination { + display: flex; + justify-content: center; + margin: 2rem 0; +} + +.pagination-list { + display: flex; + align-items: center; + gap: 0.25rem; + list-style: none; + margin: 0; + padding: 0; +} + +.pagination-link { + display: flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + height: 2.5rem; + padding: 0.5rem; + border: none; + border-radius: 0.25rem; + background-color: transparent; + color: #616161; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; + text-decoration: none; +} + +.pagination-link:hover:not(.pagination-disabled):not(.pagination-current) { + background-color: #f5f5f5; +} + +.pagination-link:focus { + outline: 2px solid #1976d2; + outline-offset: 2px; +} + +.pagination-disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.pagination-current { + background-color: #1976d2; + color: white; +} + +.pagination-current:hover { + background-color: #1e88e5; + color: white; +} + +.pagination-page { + /* Inherits from .pagination-link */ +} + +.pagination-prev-next { + font-size: 1rem; + font-weight: 500; +} + +.pagination-ellipsis { + display: flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + height: 2.5rem; + padding: 0.5rem; + color: #9e9e9e; + font-size: 0.875rem; + font-weight: 500; +} + +.pagination-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .pagination-link { + color: #e0e0e0; + } + + .pagination-link:hover:not(.pagination-disabled):not(.pagination-current) { + background-color: #2c2c2c; + } + + .pagination-current { + background-color: #1976d2; + color: white; + } + + .pagination-current:hover { + background-color: #1565c0; + color: white; + } + + .pagination-ellipsis { + color: #757575; + } +} + +/* Mobile responsive */ +@media (max-width: 640px) { + .pagination-link { + min-width: 2rem; + height: 2rem; + padding: 0.25rem; + font-size: 0.75rem; + } + + .pagination-ellipsis { + min-width: 2rem; + height: 2rem; + padding: 0.25rem; + font-size: 0.75rem; + } + + .pagination-pagination { + margin: 1rem 0; + } + + .pagination-list { + gap: 0.125rem; + } +} + + +/* ui/components/Spinner/Spinner.css */ +.spinner-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + text-align: center; +} + +.spinner-spinner { + width: 2rem; + height: 2rem; + border: 3px solid #e0e0e0; + border-top: 3px solid #1976d2; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + + +/* ui/pages/Character/Character.css */ +/* DISCLAIMER! VIBECODED! */ +.character-container { + margin: 0 auto; + padding: 1rem; + min-height: 60vh; +} + +/* Error state */ +.character-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.character-error h2 { + color: #c62828; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; +} + +.character-error p { + color: #757575; + font-size: 1rem; + margin: 0; +} + +/* Episodes section */ +.character-episodes-section { + margin-top: 2rem; +} + +.character-episodes-title { + font-size: 1.5rem; + font-weight: 500; + margin: 0 0 1.5rem 0; + color: #212121; +} + +.character-episodes-title a { + color: inherit; + text-decoration: none; + transition: color 0.2s ease; +} + +.character-episodes-title a:hover { + color: #1976d2; + text-decoration: underline; +} + +/* Responsive design */ +@media (max-width: 768px) { + .character-container { + padding: 0.75rem; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .character-error p { + color: #bdbdbd; + } + + .character-episodes-title { + color: #e0e0e0; + } + + .character-episodes-title a:hover { + color: #42a5f5; + } +} + + +/* ui/pages/Characters/Characters.css */ +/* DISCLAIMER! VIBECODED! */ +.characters-container { + margin: 0 auto; + padding: 1rem; +} + +/* Error state */ +.characters-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.characters-error h2 { + color: #dc2626; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; +} + +.characters-error p { + color: #6b7280; + font-size: 1rem; + margin: 0; +} + +/* Empty state */ +.characters-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.characters-empty h2 { + color: #374151; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; +} + +.characters-empty p { + color: #6b7280; + font-size: 1rem; + margin: 0; +} + + +/* ui/pages/Episode/Episode.css */ +/* DISCLAIMER! VIBECODED! */ +.episode-container { + margin: 0 auto; + padding: 1rem; + min-height: 60vh; +} + +/* Loading state */ +/* Error state */ +.episode-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.episode-error h2 { + color: #c62828; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; +} + +.episode-error p { + color: #757575; + font-size: 1rem; + margin: 0; +} + +/* Characters section */ +.episode-characters-section { + margin-top: 3rem; +} + +.episode-characters-title { + font-size: 1.5rem; + font-weight: 500; + margin: 0 0 1.5rem 0; + color: #212121; +} + +.episode-characters-title a { + color: inherit; + text-decoration: none; + transition: color 0.2s ease; +} + +.episode-characters-title a:hover { + color: #1976d2; + text-decoration: underline; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .episode-error p { + color: #bdbdbd; + } + + .episode-characters-title { + color: #e0e0e0; + } + + .episode-characters-title a:hover { + color: #42a5f5; + } +} + +/* Responsive design */ +@media (max-width: 768px) { + .episode-container { + padding: 0.75rem; + } +} + + +/* ui/pages/Episodes/Episodes.css */ +/* DISCLAIMER! VIBECODED! */ +.episodes-container { + margin: 0 auto; + padding: 1rem; +} + +/* Loading state */ +/* Error state */ +.episodes-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.episodes-error h2 { + color: #dc2626; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; +} + +.episodes-error p { + color: #6b7280; + font-size: 1rem; + margin: 0; +} + + +/* ui/pages/Location/Location.css */ +/* DISCLAIMER! VIBECODED! */ +.location-container { + margin: 0 auto; + padding: 1rem; + min-height: 60vh; +} + +/* Error state */ +.location-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.location-error h2 { + color: #c62828; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; +} + +.location-error p { + color: #757575; + font-size: 1rem; + margin: 0; +} + +/* Residents section */ +.location-residents-section { + margin-top: 3rem; +} + +.location-residents-title { + font-size: 1.5rem; + font-weight: 500; + margin: 0 0 1.5rem 0; + color: #212121; +} + +.location-residents-title a { + color: inherit; + text-decoration: none; + transition: color 0.2s ease; +} + +.location-residents-title a:hover { + color: #1976d2; + text-decoration: underline; +} + +/* Responsive design */ +@media (max-width: 768px) { + .location-container { + padding: 0.75rem; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .location-error p { + color: #bdbdbd; + } + + .location-residents-title { + color: #e0e0e0; + } + + .location-residents-title a:hover { + color: #42a5f5; + } +} + + +/* ui/pages/Locations/Locations.css */ +/* DISCLAIMER! VIBECODED! */ +.locations-container { + margin: 0 auto; + padding: 1rem; +} + +/* Error state */ +.locations-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.locations-error h2 { + color: #dc2626; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; +} + +.locations-error p { + color: #6b7280; + font-size: 1rem; + margin: 0; +} + +/* Empty state */ +.locations-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.locations-empty h2 { + color: #374151; + font-size: 1.5rem; + margin: 0 0 0.5rem 0; +} + +.locations-empty p { + color: #6b7280; + font-size: 1rem; + margin: 0; +} + +/* Responsive design */ +@media (max-width: 768px) { + .locations-container { + padding: 0.75rem; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .locations-error p { + color: #9ca3af; + } + + .locations-empty h2 { + color: #f9fafb; + } + + .locations-empty p { + color: #9ca3af; + } +} +/* rick-and-morty css modules migrated end */ diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/layout.module.css b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/layout.module.css deleted file mode 100644 index edf81c6e..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/layout.module.css +++ /dev/null @@ -1,11 +0,0 @@ -/* DISCLAIMER! VIBECODED! */ -/* App - Material Design */ -.app { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -.main { - flex: 1; -} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/layout.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/layout.tsx index 23789c68..76e240b5 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/layout.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/layout.tsx @@ -6,7 +6,6 @@ import { import { NextNavigation } from '@nano_kit/next-router' import { routes } from '@/stores/router' import { Header } from '@/ui/blocks/Header' -import styles from './layout.module.css' import './globals.css' declare module '@nano_kit/router' { @@ -31,9 +30,9 @@ export default function RootLayout({ children }: { -
+
-
+
{children}
diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/location/[locationId]/page.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/location/[locationId]/page.tsx index baf39703..278d0a3e 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/location/[locationId]/page.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/location/[locationId]/page.tsx @@ -1,7 +1,8 @@ import { Dehydration } from '@nano_kit/react' import { NextNavigation } from '@nano_kit/next-router' import { routes } from '@/stores/router' -import LocationPage, { Stores$ } from '@/ui/pages/Location' +import LocationPage from '@/ui/pages/Location' +import { Stores$ } from '@/ui/pages/Location.stores' export default function Page() { return ( diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/locations/page.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/locations/page.tsx index dbd9ba22..6f7e6993 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/locations/page.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/app/locations/page.tsx @@ -1,7 +1,8 @@ import { Dehydration } from '@nano_kit/react' import { NextNavigation } from '@nano_kit/next-router' import { routes } from '@/stores/router' -import LocationsPage, { Stores$ } from '@/ui/pages/Locations' +import LocationsPage from '@/ui/pages/Locations' +import { Stores$ } from '@/ui/pages/Locations.stores' export default function Page() { return ( diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/common/constants.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/common/constants.ts index a6ebc3c0..8af7608c 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/common/constants.ts +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/common/constants.ts @@ -1 +1,2 @@ export const OK_STATUS = 200 +export const STALE_TIME = 3000_000 // 5 minutes diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/services/api.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/services/api.ts index d1bb1550..7a238370 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/services/api.ts +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/services/api.ts @@ -11,7 +11,7 @@ import type { export * from './types' -const BASE_URL = 'https://rickandmortyapi.com/api' +const BASE_URL = 'https://trigensoftware.github.io/rick-and-morty-api/api' async function fetchApi(url: string): Promise> { try { @@ -42,107 +42,37 @@ async function fetchApi(url: string): Promise> { } export async function getCharacters(filters?: CharacterFilter): Promise>> { - const params = new URLSearchParams() - - if (filters?.page) { - params.append('page', filters.page.toString()) - } - - if (filters?.name) { - params.append('name', filters.name) - } - - if (filters?.status) { - params.append('status', filters.status) - } - - if (filters?.species) { - params.append('species', filters.species) - } - - if (filters?.type) { - params.append('type', filters.type) - } - - if (filters?.gender) { - params.append('gender', filters.gender) - } - - const queryString = params.toString() - const url = `${BASE_URL}/character${queryString ? `?${queryString}` : ''}` + return await fetchApi(`${BASE_URL}/character/page/${filters?.page || 1}.json`) +} - return await fetchApi>(url) +export async function getCharacter(id: number): Promise> { + return await fetchApi(`${BASE_URL}/character/${id}.json`) } -export async function getCharacter( - id: T -): Promise> { - const ids = Array.isArray(id) ? id.join(',') : id - const url = `${BASE_URL}/character/${ids}` +export async function getLocationResidents(id: number): Promise> { + return await fetchApi(`${BASE_URL}/location/residents/${id}.json`) +} - return await fetchApi(url) +export async function getEpisodeCharacters(id: number): Promise> { + return await fetchApi(`${BASE_URL}/episode/characters/${id}.json`) } export async function getLocations(filters?: LocationFilter): Promise>> { - const params = new URLSearchParams() - - if (filters?.page) { - params.append('page', filters.page.toString()) - } - - if (filters?.name) { - params.append('name', filters.name) - } - - if (filters?.type) { - params.append('type', filters.type) - } - - if (filters?.dimension) { - params.append('dimension', filters.dimension) - } - - const queryString = params.toString() - const url = `${BASE_URL}/location${queryString ? `?${queryString}` : ''}` - - return await fetchApi>(url) + return await fetchApi(`${BASE_URL}/location/page/${filters?.page || 1}.json`) } -export async function getLocation( - id: T -): Promise> { - const ids = Array.isArray(id) ? id.join(',') : id - const url = `${BASE_URL}/location/${ids}` - - return await fetchApi(url) +export async function getLocation(id: number): Promise> { + return await fetchApi(`${BASE_URL}/location/${id}.json`) } export async function getEpisodes(filters?: EpisodeFilter): Promise>> { - const params = new URLSearchParams() - - if (filters?.page) { - params.append('page', filters.page.toString()) - } - - if (filters?.name) { - params.append('name', filters.name) - } - - if (filters?.episode) { - params.append('episode', filters.episode) - } - - const queryString = params.toString() - const url = `${BASE_URL}/episode${queryString ? `?${queryString}` : ''}` - - return await fetchApi>(url) + return await fetchApi(`${BASE_URL}/episode/page/${filters?.page || 1}.json`) } -export async function getEpisode( - id: T -): Promise> { - const ids = Array.isArray(id) ? id.join(',') : id - const url = `${BASE_URL}/episode/${ids}` +export async function getEpisode(id: number): Promise> { + return await fetchApi(`${BASE_URL}/episode/${id}.json`) +} - return await fetchApi(url) +export async function getCharacterEpisodes(id: number): Promise> { + return await fetchApi(`${BASE_URL}/character/episodes/${id}.json`) } diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/services/types.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/services/types.ts index ad5612b9..7e54fdf8 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/services/types.ts +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/services/types.ts @@ -12,25 +12,15 @@ export interface ResourceBase { } export interface CharacterFilter { - name?: string - type?: string - species?: string - status?: string - gender?: string page?: number } export interface LocationFilter { - name?: string - type?: string page?: number - dimension?: string } export interface EpisodeFilter { - name?: string page?: number - episode?: string } export interface Character extends ResourceBase { diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/characters.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/characters.ts index 3c92f804..e6b796ff 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/characters.ts +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/characters.ts @@ -1,21 +1,18 @@ -import { - computed, - inject -} from '@nano_kit/store' +import { inject } from '@nano_kit/store' import { queryKey } from '@nano_kit/query' import { type Character, + getEpisodeCharacters, getCharacter, - getCharacters -} from '@/services/api' -import { OK_STATUS } from '@/common/constants' + getCharacters, + getLocationResidents +} from '../services/api' +import { OK_STATUS } from '../common/constants' import { type Page, Client$ } from './query' import { Params$ } from './router' -import { Location$ } from './locations' -import { Episode$ } from './episodes' export function Characters$() { const { query } = inject(Client$) @@ -95,43 +92,27 @@ export function Residents$() { $locationId, $episodeId } = inject(Params$) - const { $location } = inject(Location$) - const { $episode } = inject(Episode$) - const $residentsIds = computed(() => { - const locationId = $locationId() - const episodeId = $episodeId() - let refs - - if (locationId) { - refs = $location()?.residents - } else if (episodeId) { - refs = $episode()?.characters - } - - return refs?.map((ref) => { - const parts = ref.split('/') - - return Number(parts[parts.length - 1]) - }).sort() || [] - }) const [ $residents, $residentsError, $residentsLoading - ] = query<[ids: number[]], Character[]>( + ] = query<[locationId: number | null, episodeId: number | null], Character[]>( queryKey('residents'), - [$residentsIds], - async (ids) => { - if (ids.length === 0) { + [ + $locationId, + $episodeId + ], + async (locationId, episodeId) => { + if (!locationId && !episodeId) { return [] } - const response = await getCharacter(ids) + const response = locationId + ? await getLocationResidents(locationId) + : await getEpisodeCharacters(episodeId as number) if (response.status === OK_STATUS) { - return Array.isArray(response.data) - ? response.data - : [response.data] + return response.data } throw new Error(response.statusMessage) diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/episodes.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/episodes.ts index 0d2c9b8d..56aeb1c4 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/episodes.ts +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/episodes.ts @@ -1,20 +1,17 @@ -import { - inject, - computed -} from '@nano_kit/store' +import { inject } from '@nano_kit/store' import { queryKey } from '@nano_kit/query' import { type Episode, + getCharacterEpisodes, getEpisode, getEpisodes -} from '@/services/api' -import { OK_STATUS } from '@/common/constants' +} from '../services/api' +import { OK_STATUS } from '../common/constants' import { type Page, Client$ } from './query' import { Params$ } from './router' -import { Character$ } from './characters' export function Episodes$() { const { query } = inject(Client$) @@ -91,39 +88,22 @@ export function Episode$() { export function CharacterEpisodes$() { const { query } = inject(Client$) const { $characterId } = inject(Params$) - const { $character } = inject(Character$) - const $episodesIds = computed(() => { - const characterId = $characterId() - let episodes - - if (characterId) { - episodes = $character()?.episode - } - - return episodes?.map((ep) => { - const parts = ep.split('/') - - return Number(parts[parts.length - 1]) - }).sort() || [] - }) const [ $characterEpisodes, $characterEpisodesError, $characterEpisodesLoading - ] = query<[ids: number[]], Episode[]>( + ] = query<[characterId: number | null], Episode[]>( queryKey('characterEpisodes'), - [$episodesIds], - async (ids) => { - if (ids.length === 0) { + [$characterId], + async (characterId) => { + if (!characterId) { return [] } - const response = await getEpisode(ids) + const response = await getCharacterEpisodes(characterId) if (response.status === OK_STATUS) { - return Array.isArray(response.data) - ? response.data - : [response.data] + return response.data } throw new Error(response.statusMessage) diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/locations.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/locations.ts index c097cb61..90686b2d 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/locations.ts +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/locations.ts @@ -4,8 +4,8 @@ import { type Location, getLocation, getLocations -} from '@/services/api' -import { OK_STATUS } from '@/common/constants' +} from '../services/api' +import { OK_STATUS } from '../common/constants' import { type Page, Client$ diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/query.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/query.ts index ea623fdf..99020ac8 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/query.ts +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/stores/query.ts @@ -1,10 +1,8 @@ import { client, dedupeTime, - persistence, ssr } from '@nano_kit/query' -import { memoryStorage } from './memoryStorage' export interface Page { items: T[] @@ -12,12 +10,10 @@ export interface Page { } const DEDUPE_TIME = 3000_000 // 5 minutes -const storage = typeof window === 'undefined' ? memoryStorage() : null export function Client$() { return client( dedupeTime(DEDUPE_TIME), - ssr(), - persistence(storage, Infinity) + ssr() ) } diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard.tsx new file mode 100644 index 00000000..6b4916a4 --- /dev/null +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard.tsx @@ -0,0 +1,70 @@ +/* DISCLAIMER! VIBECODED! */ +'use client' +import clsx from 'clsx' +import { Link } from '@nano_kit/next-router' +import { type Character } from '@/services/api' + +export interface CharacterCardProps { + character: Character +} + +export function CharacterCard({ character }: CharacterCardProps) { + return ( +
+ +
+ {character.name} +
+ +
+

{character.name}

+ +
+
+ Status: + + {character.status} + +
+ +
+ Species: + {character.species} +
+ +
+ Gender: + {character.gender} +
+ +
+ Origin: + {character.origin.name} +
+ +
+ Location: + {character.location.name} +
+ +
+ Episodes: + {character.episode.length} +
+
+
+ +
+ ) +} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.module.css b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.module.css deleted file mode 100644 index c55f195b..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.module.css +++ /dev/null @@ -1,166 +0,0 @@ -/* DISCLAIMER! VIBECODED! */ -/* Character Card - Material Design */ -.card { - background: #ffffff; - border-radius: 0.5rem; - overflow: hidden; - transition: box-shadow 0.2s ease; - cursor: pointer; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); -} - -.card:hover { - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); -} - -.link { - display: block; - text-decoration: none; - color: inherit; -} - -.imageWrapper { - position: relative; - aspect-ratio: 1; - overflow: hidden; - background: #f5f5f5; -} - -.image { - width: 100%; - height: 100%; - object-fit: cover; -} - -.content { - padding: 1rem; -} - -.name { - font-size: 1.125rem; - font-weight: 500; - color: #212121; - margin: 0 0 1rem 0; - line-height: 1.4; -} - -.info { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.row { - display: flex; - justify-content: space-between; - align-items: center; -} - -.label { - font-weight: 400; - color: #757575; - font-size: 0.875rem; -} - -.value { - font-weight: 500; - color: #424242; - font-size: 0.875rem; - text-align: right; -} - -/* Status indicators */ -.status { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - font-size: 0.75rem; - font-weight: 500; - text-transform: uppercase; -} - -.status--alive { - background-color: #e8f5e8; - color: #2e7d32; -} - -.status--dead { - background-color: #ffebee; - color: #c62828; -} - -.status--unknown { - background-color: #f5f5f5; - color: #757575; -} - -/* Dark mode support */ -@media (prefers-color-scheme: dark) { - .card { - background: #424242; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24), 0 1px 2px rgba(0, 0, 0, 0.32); - } - - .card:hover { - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.32), 0 3px 6px rgba(0, 0, 0, 0.32); - } - - .imageWrapper { - background: #616161; - } - - .name { - color: #ffffff; - } - - .label { - color: #bdbdbd; - } - - .value { - color: #e0e0e0; - } - - .status--alive { - background-color: #1b5e20; - color: #a5d6a7; - } - - .status--dead { - background-color: #b71c1c; - color: #ef9a9a; - } - - .status--unknown { - background-color: #616161; - color: #bdbdbd; - } -} - -/* Responsive design */ -@media (max-width: 768px) { - .content { - padding: 0.875rem; - } - - .name { - font-size: 1rem; - margin-bottom: 0.875rem; - } - - .row { - flex-direction: column; - align-items: flex-start; - gap: 0.25rem; - } - - .value { - text-align: left; - font-size: 0.8125rem; - } - - .label { - font-size: 0.8125rem; - } -} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.tsx deleted file mode 100644 index a3bce6c3..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/CharacterCard.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* DISCLAIMER! VIBECODED! */ -'use client' -import clsx from 'clsx' -import { Link } from '@nano_kit/next-router' -import { type Character } from '@/services/api' -import styles from './CharacterCard.module.css' - -export interface CharacterCardProps { - character: Character -} - -export function CharacterCard({ character }: CharacterCardProps) { - return ( -
- -
- {character.name} -
- -
-

{character.name}

- -
-
- Status: - - {character.status} - -
- -
- Species: - {character.species} -
- -
- Gender: - {character.gender} -
- -
- Origin: - {character.origin.name} -
- -
- Location: - {character.location.name} -
- -
- Episodes: - {character.episode.length} -
-
-
- -
- ) -} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/index.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/index.ts deleted file mode 100644 index 8dfcdef3..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CharacterCard' diff --git a/examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail.tsx similarity index 52% rename from examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.tsx rename to examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail.tsx index e0e37019..9f289e1f 100644 --- a/examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail.tsx @@ -5,7 +5,6 @@ import { useInject } from '@nano_kit/react' import { Character$ } from '@/stores/characters' -import styles from './CharacterDetail.module.css' export function CharacterDetail() { const { $character } = useInject(Character$) @@ -16,38 +15,38 @@ export function CharacterDetail() { } return ( -
-
+
+
{character.name} -
-

{character.name}

-
- +
+

{character.name}

+
+ {character.status} - {character.species}
{character.type - &&

Type: {character.type}

+ &&

Type: {character.type}

} -

Gender: {character.gender}

+

Gender: {character.gender}

-
-
+
+

Origin

{character.origin.name}

-
+

Last known location

{character.location.name}

-
+

Episodes

{character.episode.length} episodes

diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.module.css b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.module.css deleted file mode 100644 index 22cdda8c..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail/CharacterDetail.module.css +++ /dev/null @@ -1,154 +0,0 @@ -/* DISCLAIMER! VIBECODED! */ -.container { - width: 100%; -} - -/* Header */ -.header { - display: flex; - gap: 2rem; - margin-bottom: 2rem; - align-items: flex-start; -} - -.image { - width: 200px; - height: 200px; - border-radius: 12px; - object-fit: cover; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); -} - -.info { - flex: 1; -} - -.name { - font-size: 2rem; - font-weight: 500; - margin: 0 0 1rem 0; - color: #212121; -} - -.status { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.1rem; - font-weight: 500; - margin-bottom: 0.5rem; - color: #424242; -} - -.statusIndicator { - width: 10px; - height: 10px; - border-radius: 50%; -} - -.alive { - background-color: #2e7d32; -} - -.dead { - background-color: #c62828; -} - -.unknown { - background-color: #757575; -} - -.type, -.gender { - margin: 0.5rem 0; - color: #757575; - font-size: 1rem; -} - -/* Details */ -.details { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 2rem; -} - -.section { - background: #ffffff; - border-radius: 12px; - padding: 1.5rem; - border: 1px solid #e0e0e0; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); -} - -.section h2 { - font-size: 1.25rem; - font-weight: 500; - margin: 0 0 1rem 0; - color: #212121; -} - -.section p { - margin: 0; - color: #424242; - font-size: 1rem; -} - -/* Dark mode support */ -@media (prefers-color-scheme: dark) { - .name { - color: #e0e0e0; - } - - .status { - color: #bdbdbd; - } - - .type, - .gender { - color: #9e9e9e; - } - - .section { - background: #424242; - border-color: #616161; - } - - .section h2 { - color: #e0e0e0; - } - - .section p { - color: #bdbdbd; - } -} - -/* Responsive design */ -@media (max-width: 768px) { - .container { - padding: 1rem; - } - - .header { - flex-direction: column; - align-items: center; - text-align: center; - } - - .image { - width: 150px; - height: 150px; - } - - .name { - font-size: 1.75rem; - } - - .details { - grid-template-columns: 1fr; - gap: 1rem; - } - - .section { - padding: 1rem; - } -} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail/index.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail/index.ts deleted file mode 100644 index d1e8eb24..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharacterDetail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CharacterDetail' diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharactersGrid/CharactersGrid.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharactersGrid.tsx similarity index 84% rename from examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharactersGrid/CharactersGrid.tsx rename to examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharactersGrid.tsx index 84cfb80d..57b8dd1c 100644 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharactersGrid/CharactersGrid.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/CharactersGrid.tsx @@ -2,7 +2,6 @@ 'use client' import { type Character } from '@/services/api' import { CharacterCard } from '@/ui/blocks/CharacterCard' -import styles from './CharactersGrid.module.css' export interface CharactersGridProps { characters: Character[] @@ -10,7 +9,7 @@ export interface CharactersGridProps { export function CharactersGrid({ characters }: CharactersGridProps) { return ( -
+
{characters.map(character => ( + +
+

{episode.name}

+ +
+
+ Episode: + {episode.episode} +
+ +
+ Air Date: + {episode.air_date} +
+ +
+ Characters: + {episode.characters.length} +
+
+
+ + + ) +} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.module.css b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.module.css deleted file mode 100644 index 461b2496..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.module.css +++ /dev/null @@ -1,108 +0,0 @@ -/* DISCLAIMER! VIBECODED! */ -/* Episode Card - Material Design */ -.card { - background: #ffffff; - border-radius: 0.5rem; - overflow: hidden; - transition: box-shadow 0.2s ease; - cursor: pointer; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); -} - -.card:hover { - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); -} - -.link { - display: block; - text-decoration: none; - color: inherit; -} - -.content { - padding: 1rem; -} - -.name { - font-size: 1.125rem; - font-weight: 500; - color: #212121; - margin: 0 0 1rem 0; - line-height: 1.4; -} - -.info { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.row { - display: flex; - justify-content: space-between; - align-items: center; -} - -.label { - font-weight: 400; - color: #757575; - font-size: 0.875rem; -} - -.value { - font-weight: 500; - color: #424242; - font-size: 0.875rem; - text-align: right; -} - -/* Dark mode support */ -@media (prefers-color-scheme: dark) { - .card { - background: #424242; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24), 0 1px 2px rgba(0, 0, 0, 0.32); - } - - .card:hover { - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.32), 0 3px 6px rgba(0, 0, 0, 0.32); - } - - .name { - color: #ffffff; - } - - .label { - color: #bdbdbd; - } - - .value { - color: #e0e0e0; - } -} - -/* Responsive design */ -@media (max-width: 768px) { - .content { - padding: 0.875rem; - } - - .name { - font-size: 1rem; - margin-bottom: 0.875rem; - } - - .row { - flex-direction: column; - align-items: flex-start; - gap: 0.25rem; - } - - .value { - text-align: left; - font-size: 0.8125rem; - } - - .label { - font-size: 0.8125rem; - } -} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.tsx deleted file mode 100644 index 6afb042a..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/EpisodeCard.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* DISCLAIMER! VIBECODED! */ -'use client' -import { Link } from '@nano_kit/next-router' -import { type Episode } from '@/services/api' -import styles from './EpisodeCard.module.css' - -export interface EpisodeCardProps { - episode: Episode -} - -export function EpisodeCard({ episode }: EpisodeCardProps) { - return ( -
- -
-

{episode.name}

- -
-
- Episode: - {episode.episode} -
- -
- Air Date: - {episode.air_date} -
- -
- Characters: - {episode.characters.length} -
-
-
- -
- ) -} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/index.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/index.ts deleted file mode 100644 index 8d3d620a..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './EpisodeCard' diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail.tsx new file mode 100644 index 00000000..28ef2c6c --- /dev/null +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail.tsx @@ -0,0 +1,46 @@ +/* DISCLAIMER! VIBECODED! */ +'use client' +import { + useSignal, + useInject +} from '@nano_kit/react' +import { Episode$ } from '@/stores/episodes' + +export function EpisodeDetail() { + const { $episode } = useInject(Episode$) + const episode = useSignal($episode) + + if (!episode) { + return null + } + + return ( +
+
+
+

{episode.name}

+
+ Episode: + {episode.episode} +
+
+ Air Date: + {episode.air_date} +
+
+
+ +
+
+

Created

+

{new Date(episode.created).toLocaleDateString()}

+
+ +
+

URL

+

Episode #{episode.id}

+
+
+
+ ) +} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css deleted file mode 100644 index b875f925..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.module.css +++ /dev/null @@ -1,127 +0,0 @@ -/* DISCLAIMER! VIBECODED! */ -.container { - width: 100%; -} - -/* Header */ -.header { - margin-bottom: 2rem; -} - -.info { - max-width: 600px; -} - -.name { - font-size: 2.5rem; - font-weight: 500; - margin: 0 0 1.5rem 0; - color: #212121; -} - -.episode, -.airDate { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 1rem; - font-size: 1.125rem; -} - -.label { - font-weight: 500; - color: #757575; - min-width: 80px; -} - -.value { - font-weight: 500; - color: #424242; -} - -/* Details */ -.details { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 2rem; -} - -.section { - background: #ffffff; - border-radius: 12px; - padding: 1.5rem; - border: 1px solid #e0e0e0; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); -} - -.section h2 { - font-size: 1.25rem; - font-weight: 500; - margin: 0 0 1rem 0; - color: #212121; -} - -.section p { - margin: 0; - color: #424242; - font-size: 1rem; -} - -/* Dark mode support */ -@media (prefers-color-scheme: dark) { - .name { - color: #e0e0e0; - } - - .label { - color: #9e9e9e; - } - - .value { - color: #bdbdbd; - } - - .section { - background: #424242; - border-color: #616161; - } - - .section h2 { - color: #e0e0e0; - } - - .section p { - color: #bdbdbd; - } -} - -/* Responsive design */ -@media (max-width: 768px) { - .container { - padding: 1rem; - } - - .name { - font-size: 2rem; - } - - .episode, - .airDate { - flex-direction: column; - align-items: flex-start; - gap: 0.25rem; - } - - .label { - min-width: auto; - } - - .details { - grid-template-columns: 1fr; - gap: 1rem; - } - - .section { - padding: 1rem; - } -} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx deleted file mode 100644 index 26026be5..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/EpisodeDetail.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* DISCLAIMER! VIBECODED! */ -'use client' -import { - useSignal, - useInject -} from '@nano_kit/react' -import { Episode$ } from '@/stores/episodes' -import styles from './EpisodeDetail.module.css' - -export function EpisodeDetail() { - const { $episode } = useInject(Episode$) - const episode = useSignal($episode) - - if (!episode) { - return null - } - - return ( -
-
-
-

{episode.name}

-
- Episode: - {episode.episode} -
-
- Air Date: - {episode.air_date} -
-
-
- -
-
-

Created

-

{new Date(episode.created).toLocaleDateString()}

-
- -
-

URL

-

Episode #{episode.id}

-
-
-
- ) -} diff --git a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/index.ts b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/index.ts deleted file mode 100644 index 5424a2d9..00000000 --- a/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodeDetail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './EpisodeDetail' diff --git a/examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodesGrid/EpisodesGrid.tsx b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodesGrid.tsx similarity index 84% rename from examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodesGrid/EpisodesGrid.tsx rename to examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodesGrid.tsx index 51227fe8..70324fe7 100644 --- a/examples/rick-and-morty/next-pages-nano_kit-ssr/src/ui/blocks/EpisodesGrid/EpisodesGrid.tsx +++ b/examples/rick-and-morty/next-app-nano_kit-ssr/src/ui/blocks/EpisodesGrid.tsx @@ -2,7 +2,6 @@ 'use client' import { type Episode } from '@/services/api' import { EpisodeCard } from '@/ui/blocks/EpisodeCard' -import styles from './EpisodesGrid.module.css' export interface EpisodesGridProps { episodes: Episode[] @@ -10,7 +9,7 @@ export interface EpisodesGridProps { export function EpisodesGrid({ episodes }: EpisodesGridProps) { return ( -
+
{episodes.map(episode => ( -
-

- 🛸 +
+
+

+ 🛸 Rick and Morty

-