diff --git a/.cspell.json b/.cspell.json index 5945796..73f82f9 100644 --- a/.cspell.json +++ b/.cspell.json @@ -29,6 +29,8 @@ "Postgrest", "supabase", "SUPABASE", + "swiper", + "Swiper", "tanstack", "tsubomi", "vstack", diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs new file mode 100644 index 0000000..7b449f4 --- /dev/null +++ b/.dependency-cruiser.cjs @@ -0,0 +1,376 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: 'no-circular', + severity: 'warn', + comment: + 'This dependency is part of a circular relationship. You might want to revise ' + + 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', + from: {}, + to: { + circular: true + } + }, + { + name: 'no-orphans', + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files + '[.]d[.]ts$', // TypeScript declaration files + '(^|/)tsconfig[.]json$', // TypeScript config + '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs + ] + }, + to: {}, + }, + { + name: 'no-deprecated-core', + comment: + 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "bound to exist - node doesn't deprecate lightly.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'core' + ], + path: [ + '^v8/tools/codemap$', + '^v8/tools/consarray$', + '^v8/tools/csvparser$', + '^v8/tools/logreader$', + '^v8/tools/profile_view$', + '^v8/tools/profile$', + '^v8/tools/SourceMap$', + '^v8/tools/splaytree$', + '^v8/tools/tickprocessor-driver$', + '^v8/tools/tickprocessor$', + '^node-inspect/lib/_inspect$', + '^node-inspect/lib/internal/inspect_client$', + '^node-inspect/lib/internal/inspect_repl$', + '^async_hooks$', + '^punycode$', + '^domain$', + '^constants$', + '^sys$', + '^_linklist$', + '^_stream_wrap$' + ], + } + }, + { + name: 'not-to-deprecated', + comment: + 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + + 'version of that module, or find an alternative. Deprecated modules are a security risk.', + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'deprecated' + ] + } + }, + { + name: 'no-non-package-json', + severity: 'error', + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: [ + 'npm-no-pkg', + 'npm-unknown' + ] + } + }, + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + 'module: add it to your package.json. In all other cases you likely already know what to do.', + severity: 'error', + from: {}, + to: { + couldNotResolve: true + } + }, + { + name: 'no-duplicate-dep-types', + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: 'warn', + from: {}, + to: { + moreThanOneDependencyType: true, + // as it's pretty common to have a type import be a type only import + // _and_ (e.g.) a devDependency - don't consider type-only dependency + // types for this rule + dependencyTypesNot: ["type-only"] + } + }, + + /* rules you might want to tweak for your specific situation: */ + + { + name: 'not-to-spec', + comment: + 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', + severity: 'error', + from: {}, + to: { + path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + } + }, + { + name: 'not-to-dev-dep', + severity: 'error', + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + 'section of your package.json. If this module is development only - add it to the ' + + 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + from: { + path: '^(src)', + pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + }, + to: { + dependencyTypes: [ + 'npm-dev', + ], + // type only dependencies are not a problem as they don't end up in the + // production code or are ignored by the runtime. + dependencyTypesNot: [ + 'type-only' + ], + pathNot: [ + 'node_modules/@types/' + ] + } + }, + { + name: 'optional-deps-used', + severity: 'info', + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you're using an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: [ + 'npm-optional' + ] + } + }, + { + name: 'peer-deps-used', + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'npm-peer' + ] + } + } + ], + options: { + + /* Which modules not to follow further when encountered */ + doNotFollow: { + /* path: an array of regular expressions in strings to match against */ + path: ['node_modules'] + }, + + /* Which modules to exclude */ + // exclude : { + // /* path: an array of regular expressions in strings to match against */ + // path: '', + // }, + + /* Which modules to exclusively include (array of regular expressions in strings) + dependency-cruiser will skip everything not matching this pattern + */ + // includeOnly : [''], + + /* List of module systems to cruise. + When left out dependency-cruiser will fall back to the list of _all_ + module systems it knows of. It's the default because it's the safe option + It might come at a performance penalty, though. + moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] + + As in practice only commonjs ('cjs') and ecmascript modules ('es6') + are widely used, you can limit the moduleSystems to those. + */ + + // moduleSystems: ['cjs', 'es6'], + + /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' + to open it on your online repo or `vscode://file/${process.cwd()}/` to + open it in visual studio code), + */ + // prefix: `vscode://file/${process.cwd()}/`, + + /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation + true: also detect dependencies that only exist before typescript-to-javascript compilation + "specify": for each dependency identify whether it only exists before compilation or also after + */ + tsPreCompilationDeps: true, + + /* list of extensions to scan that aren't javascript or compile-to-javascript. + Empty by default. Only put extensions in here that you want to take into + account that are _not_ parsable. + */ + // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"], + + /* if true combines the package.jsons found from the module up to the base + folder the cruise is initiated from. Useful for how (some) mono-repos + manage dependencies & dependency definitions. + */ + // combinedDependencies: false, + + /* if true leave symlinks untouched, otherwise use the realpath */ + // preserveSymlinks: false, + + /* TypeScript project file ('tsconfig.json') to use for + (1) compilation and + (2) resolution (e.g. with the paths property) + + The (optional) fileName attribute specifies which file to take (relative to + dependency-cruiser's current working directory). When not provided + defaults to './tsconfig.json'. + */ + tsConfig: { + fileName: 'tsconfig.json' + }, + + /* Webpack configuration to use to get resolve options from. + + The (optional) fileName attribute specifies which file to take (relative + to dependency-cruiser's current working directory. When not provided defaults + to './webpack.conf.js'. + + The (optional) `env` and `arguments` attributes contain the parameters + to be passed if your webpack config is a function and takes them (see + webpack documentation for details) + */ + // webpackConfig: { + // fileName: 'webpack.config.js', + // env: {}, + // arguments: {} + // }, + + /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use + for compilation + */ + // babelConfig: { + // fileName: '.babelrc', + // }, + + /* List of strings you have in use in addition to cjs/ es6 requires + & imports to declare module dependencies. Use this e.g. if you've + re-declared require, use a require-wrapper or use window.require as + a hack. + */ + // exoticRequireStrings: [], + + /* options to pass on to enhanced-resolve, the package dependency-cruiser + uses to resolve module references to disk. The values below should be + suitable for most situations + + If you use webpack: you can also set these in webpack.conf.js. The set + there will override the ones specified here. + */ + enhancedResolveOptions: { + /* What to consider as an 'exports' field in package.jsons */ + exportsFields: ["exports"], + /* List of conditions to check for in the exports field. + Only works when the 'exportsFields' array is non-empty. + */ + conditionNames: ["import", "require", "node", "default", "types"], + /* + The extensions, by default are the same as the ones dependency-cruiser + can access (run `npx depcruise --info` to see which ones that are in + _your_ environment). If that list is larger than you need you can pass + the extensions you actually use (e.g. [".js", ".jsx"]). This can speed + up module resolution, which is the most expensive step. + */ + // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + /* What to consider a 'main' field in package.json */ + mainFields: ["module", "main", "types", "typings"], + /* + A list of alias fields in package.jsons + See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and + the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) + documentation + + Defaults to an empty array (= don't use alias fields). + */ + // aliasFields: ["browser"], + }, + reporterOptions: { + dot: { + /* pattern of modules that can be consolidated in the detailed + graphical dependency graph. The default pattern in this configuration + collapses everything in node_modules to one folder deep so you see + the external modules, but their innards. + */ + collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + /* Options to tweak the appearance of your graph.See + https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions + for details and some examples. If you don't specify a theme + dependency-cruiser falls back to a built-in one. + */ + // theme: { + // graph: { + // /* splines: "ortho" gives straight lines, but is slow on big graphs + // splines: "true" gives bezier curves (fast, not as nice as ortho) + // */ + // splines: "true" + // }, + // } + }, + archi: { + /* pattern of modules that can be consolidated in the high level + graphical dependency graph. If you use the high level graphical + dependency graph reporter (`archi`) you probably want to tweak + this collapsePattern to your situation. + */ + collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + /* Options to tweak the appearance of your graph. If you don't specify a + theme for 'archi' dependency-cruiser will use the one specified in the + dot section above and otherwise use the default one. + */ + // theme: { }, + }, + "text": { + "highlightFocused": true + }, + } + } +}; +// generated: dependency-cruiser@16.5.0 on 2024-11-02T11:43:49.038Z diff --git a/.eslintignore b/.eslintignore index 4671a75..4460f06 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ vite.config.mts src/vite-env.d.ts vite.config.ts panda/ +node_modules/ diff --git a/.eslintrc.json b/.eslintrc.json index f73914c..64c09c0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -35,6 +35,7 @@ "allowAsThisParameter": true } ], + "@typescript-eslint/naming-convention": "off", "react/react-in-jsx-scope": "off", "react/jsx-filename-extension": "off", "react/jsx-uses-react": "off", @@ -47,6 +48,7 @@ "import/prefer-default-export": "off", "import/extensions": "off", "import/no-unresolved": "off", + "import/no-cycle": "off", "import/order": [ "error", { diff --git a/.gitignore b/.gitignore index 018314f..e050f89 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ src/routeTree.gen.ts .env.keys tsconfig.tsbuildinfo .wrangler/ +/deps.dot diff --git a/bun.lockb b/bun.lockb index 7fdd6b7..9e2e06d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/functions/address.ts b/functions/address.ts index 4445bd9..f0584d2 100644 --- a/functions/address.ts +++ b/functions/address.ts @@ -37,5 +37,14 @@ export const onRequest: PagesFunction = async ({ request, env }) => { appid: env.YAHOO_REV_GEO_API_KEY, }); - return await fetch(`${url}?${params.toString()}`); + const response = await fetch(`${url}?${params.toString()}`); + + const modifiedHeaders = new Headers(response.headers); + modifiedHeaders.set("Cache-Control", "public, max-age=3600"); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: modifiedHeaders, + }); }; diff --git a/index.html b/index.html index a0960d5..9c25a66 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,25 @@ - + Wakaba + + + + + + + + +
diff --git a/package.json b/package.json index d3ad560..416e5b8 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "supabase:link": "dotenvx run -- bun run ./scripts/supabase-link.ts", "supabase:start": "bun supabase start -x imgproxy,inbucket,pgadmin-schema-diff,edge-runtime,logflare,vector,pgbouncer", "supabase:seed": "bun supabase db dump --local --data-only -f ./supabase/seed.sql", - "supabase:gen-type": "bunx supabase gen types typescript --local > ./src/types/supabase.gen.ts" + "supabase:gen-type": "bunx supabase gen types typescript --local > ./src/types/supabase.gen.ts", + "deps": "bun depcruise src --include-only \"^src\" --output-type dot > deps.dot" }, "dependencies": { "@ark-ui/react": "^3.12.0", @@ -40,6 +41,7 @@ "neverthrow": "^8.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", "react-hook-form": "^7.53.0", "react-intersection-observer": "^9.13.1", "react-leaflet": "^4.2.1", @@ -66,6 +68,7 @@ "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.5.0", "cspell": "^8.14.2", + "dependency-cruiser": "^16.5.0", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^18.0.0", diff --git a/public/ogp.png b/public/ogp.png new file mode 100644 index 0000000..62355aa Binary files /dev/null and b/public/ogp.png differ diff --git a/src/assets/svg/background/circle.svg b/src/assets/svg/background/circle.svg new file mode 100644 index 0000000..7b681b7 --- /dev/null +++ b/src/assets/svg/background/circle.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/background/decoration-right.svg b/src/assets/svg/background/decoration-right.svg new file mode 100644 index 0000000..0abf4e7 --- /dev/null +++ b/src/assets/svg/background/decoration-right.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/background/header-left.svg b/src/assets/svg/background/header-left.svg new file mode 100644 index 0000000..e8e6a3d --- /dev/null +++ b/src/assets/svg/background/header-left.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/background/header-right.svg b/src/assets/svg/background/header-right.svg new file mode 100644 index 0000000..4e6e71d --- /dev/null +++ b/src/assets/svg/background/header-right.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/icon/wakaba.svg b/src/assets/svg/icon/wakaba.svg new file mode 100644 index 0000000..f6121ad --- /dev/null +++ b/src/assets/svg/icon/wakaba.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/GridLayout.tsx b/src/components/GridLayout.tsx deleted file mode 100644 index 2731e0b..0000000 --- a/src/components/GridLayout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { styled as p } from "panda/jsx"; -import { type ReactElement } from "react"; - -export function GridLayout({ - children, -}: { - children: ReactElement; -}): ReactElement { - return ( - - {children} - - ); -} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 28c8466..df63af0 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -64,13 +64,9 @@ function UserIndicator(): ReactElement { - - - {user.metadata.email} - - - {user.id} - + + {user.metadata.email} + {user.id} - {description} - + + + {new Date(seed.data.created_at).toLocaleDateString()} + + + + + + {match(swrSeedAbout) + .with(S.Success, ({ data: { category } }) => ( + # {category.data.name} + )) + .otherwise(() => null)} + + + {seed.data.description} + ); } diff --git a/src/routes/_auth/seeds/-components/SownSeedInline.tsx b/src/routes/_auth/seeds/-components/SownSeedInline.tsx new file mode 100644 index 0000000..3593e00 --- /dev/null +++ b/src/routes/_auth/seeds/-components/SownSeedInline.tsx @@ -0,0 +1,62 @@ +import { styled as p, HStack, VStack } from "panda/jsx"; +import { type ReactElement } from "react"; +import useSWRImmutable from "swr/immutable"; +import { match } from "ts-pattern"; +import { IconText } from "@/components/IconText"; +import { type Seed } from "@/lib/classes/seed"; +import { S } from "@/lib/utils/patterns"; +import { notifyTableErrorInToast } from "@/lib/utils/table"; + +export function SownSeedInline({ seed }: { seed: Seed }): ReactElement { + const swrSeedAbout = useSWRImmutable(`seed-${seed.data.seed_id}`, async () => + ( + await seed + .resolveRelation() + .mapErr(notifyTableErrorInToast("swrSeedAbout")) + )._unsafeUnwrap(), + ); + + return ( + + + + + {match(swrSeedAbout) + .with(S.Success, ({ data: { category } }) => ( + # {category.data.name} + )) + .otherwise(() => null)} + + + + {new Date(seed.data.created_at).toLocaleDateString("ja-JP")} + + + + {seed.data.description} + + + ); +} diff --git a/src/routes/_auth/seeds/index.tsx b/src/routes/_auth/seeds/index.tsx index e02914b..66a2946 100644 --- a/src/routes/_auth/seeds/index.tsx +++ b/src/routes/_auth/seeds/index.tsx @@ -1,37 +1,192 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; -import { styled as p } from "panda/jsx"; +import { HoverCard, Portal } from "@ark-ui/react"; +import { createFileRoute } from "@tanstack/react-router"; +import { ResultAsync } from "neverthrow"; +import { css } from "panda/css"; +import { styled as p, VStack } from "panda/jsx"; +import { type ReactElement } from "react"; +import useSWRImmutable from "swr/immutable"; +import { match } from "ts-pattern"; import { SownSeed } from "./-components/SownSeed"; -import { seedsData, projectsData } from "@/assets/data"; -import { GridLayout } from "@/components/GridLayout"; +import { SownSeedInline } from "./-components/SownSeedInline"; +import { ErrorScreen } from "@/components/ErrorScreen"; +import { Loading } from "@/components/Loading"; import { Button } from "@/components/cva/Button"; import { Expanded } from "@/components/cva/Expanded"; +import { GridLayout } from "@/components/cva/GridLayout"; import { ProjectCard } from "@/components/project/Card"; -import { useSession } from "@/hooks/useSession"; -import { User } from "@/lib/classes/user"; +import { svaHoverCard } from "@/components/sva/hover-card"; +import { Project } from "@/lib/classes/project"; +import { type Seed } from "@/lib/classes/seed"; +import { Sower } from "@/lib/classes/sower"; +import { S } from "@/lib/utils/patterns"; +import { notifyTableErrorInToast } from "@/lib/utils/table"; +import { type TableError, type TableResult } from "@/types/table"; -type CategoryType = "休憩" | "環境" | "飲食" | "施設" | "移動" | "その他"; +function fetchProjectSeedsMap( + seeds: Seed[], +): TableResult> { + // s_n と p(t) のペア (1-N) + const seedsTerritoriesProjects = ResultAsync.combine( + seeds.map((s) => + s.fetchTerritoriesProjects().map((tPs) => [s, tPs] as const), + ), + ).map((sTsPs) => new Map(sTsPs)); -const convertCategory = (categoryId: string): CategoryType | undefined => { - const categoryMap: Record = { - 1: "休憩", - 2: "環境", - 3: "飲食", - 4: "施設", - 5: "移動", - 6: "その他", - }; - return categoryId in categoryMap ? categoryMap[categoryId] : undefined; -}; + // p(t) と, s_n とのペア (1-N) + return seedsTerritoriesProjects.map((sTsPs) => { + const uniqueProjects = Project.getUniqueMap( + Array.from(sTsPs).flatMap(([, { projects }]) => projects), + ); + + const projectSeedsMap = new Map(); + uniqueProjects.forEach((pj) => { + projectSeedsMap.set( + pj, + Array.from(sTsPs).flatMap(([seed, { projects }]) => + projects + .filter( + ({ data: { project_id } }) => project_id === pj.data.project_id, + ) + .map(() => seed), + ), + ); + }); + + return projectSeedsMap; + }); +} -const formatCreatedAt = (createdAt: string): string => - new Date(createdAt).toLocaleDateString("ja-JP"); +function ProjectsBySeeds({ seeds }: { seeds: Seed[] }): ReactElement { + const cls = svaHoverCard(); + + const swrProjectSeedsPairs = useSWRImmutable( + "project-seeds-pairs", + async () => + ( + await fetchProjectSeedsMap(seeds).mapErr((e) => + notifyTableErrorInToast("swrProjects")({ + ...e, + message: "芽が出た種の取得に失敗しました", + }), + ) + )._unsafeUnwrap(), + ); + + return match(swrProjectSeedsPairs) + .with(S.Loading, () => 芽が出た種を読み込み中…) + .with(S.Success, ({ data }) => ( + + {Array.from(data).map(([pj, sd]) => ( + + + + + + + + + + + + + + + 元となったあなたのシード + + + {sd.map((s) => ( + + ))} + + + + + + + ))} + + )) + .otherwise(() => ( + + )); +} export const Route = createFileRoute("/_auth/seeds/")({ + loader: async ({ context }) => { + const { user } = context; + const sower = Sower.factories.fromUser(user.id); + + return ( + await sower.andThen((s) => + s.fetchOwnSeeds().map((seeds) => ({ + sower: s, + seeds, + })), + ) + ).match( + (s) => s, + (e) => { + throw new Error(e.message, { cause: e }); + }, + ); + }, + errorComponent: ({ error }) => { + const { cause } = error as Error & { cause: TableError }; + // eslint-disable-next-line no-console + console.error(cause); + + return ( + + + + ); + }, + pendingComponent: () => ( + + + シードを読み込み中… + + + ), component: () => { - const { session } = useSession(); - if (session == null) return undefined; - const user = new User(session); - const mySeedData = seedsData.filter((seed) => seed.sower_id === user.id); + const { seeds } = Route.useLoaderData(); return ( @@ -72,79 +227,20 @@ export const Route = createFileRoute("/_auth/seeds/")({ 自分が蒔いた種 - {mySeedData.length === 0 ? ( + {seeds.length === 0 ? ( まだ蒔いた種がありません。 ) : ( - -
-
- {mySeedData - .filter((_, index) => index % 2 !== 0) - .map((seed) => { - const category = convertCategory(seed.category_id); - if (category === undefined) return null; - - return ( - - - - ); - })} -
-
- {mySeedData - .filter((_, index) => index % 2 === 0) - .map((seed) => { - const category = convertCategory(seed.category_id); - if (category === undefined) return null; - - return ( - - - - ); - })} -
-
-
+ + {seeds.map((s) => ( + + ))} + )} - 芽が出た種 - - <> - {projectsData - .sort((a, b) => b.created_at.localeCompare(a.created_at)) - .map((_) => ( - - - - ))} - - +
diff --git a/src/routes/company-form/$uuid.tsx b/src/routes/company-form/$uuid.tsx index 49ee3e1..7e6fcba 100644 --- a/src/routes/company-form/$uuid.tsx +++ b/src/routes/company-form/$uuid.tsx @@ -1,776 +1,5 @@ -import { Dialog, Portal, Field, DatePicker, NumberInput } from "@ark-ui/react"; -import { createFileRoute } from "@tanstack/react-router"; -import { HStack, styled as p } from "panda/jsx"; -import { useState } from "react"; -import { useForm, type SubmitHandler } from "react-hook-form"; +import { createFileRoute } from '@tanstack/react-router' -import { - projectsData, - sponsorDataData, - sponsorsData, - seedsData, -} from "@/assets/data"; -import { Button } from "@/components/cva/Button"; -import { svaDatePicker } from "@/components/sva/datePicker"; -import { svaFormDialog } from "@/components/sva/formDialog"; -import { svaNumberInput } from "@/components/sva/numberInput"; -import { svaTextArea } from "@/components/sva/textArea"; -import { toaster } from "@/lib/utils/toast"; - -type needs = { - amount_of_money: number; - created_at: string; - deadline: string; - key_visual: string; - name: string; - project_id: string; - status: "wakaba" | "tsubomi" | "hana"; - sponsor_data_id: string; - description: string; - location: { - lat: number; - lon: number; - }; - sponsor: - | { - created_at: string; - description: string | null; - icon: string; - name: string; - sponsor_id: string; - user_id: string; - } - | undefined; - sponsor_data: - | { - target_amount_of_money: number; - location?: { - lat: number; - lon: number; - }; - motivation: string | undefined; - reports?: - | Array<{ - body: string; - key_visual: string | null; - report_id: string; - title: string; - created_at: string; - }> - | undefined; - fruits?: - | Array<{ - description: string; - key_visual: string | null; - name: string; - }> - | undefined; - } - | undefined; - seeds: - | Array<{ - category_id: string; - created_at: string; - description: string | null; - location: unknown; - seed_id: string; - sower_id: string; - }> - | undefined; -}; - -const textArea = svaTextArea(); -const datePicker = svaDatePicker(); -const numberInput = svaNumberInput(); - -export const Route = createFileRoute("/company-form/$uuid")({ - component: () => { - const { uuid } = Route.useParams(); - const dialog = svaFormDialog(); - const data2 = projectsData.find((_) => _.project_id === uuid); - if (data2 === undefined) throw new Error("No data found"); - - const data3 = sponsorDataData.find( - (_) => _.project_id === data2.project_id, - ); - - const data4 = sponsorsData.find((_) => _.sponsor_id === data3?.sponsor_id); - - const data5 = data2.seed_id.map((s) => { - const seed = seedsData.find((_) => _.seed_id === String(s)); - return seed; - }); - if (data5 === undefined) throw new Error("No seeds found"); - - const data: needs = { - amount_of_money: data2.amount_of_money, - created_at: data2.created_at, - deadline: data2.deadline, - key_visual: data2.key_visual ?? "", - name: data2.name, - project_id: data2.project_id, - sponsor_data_id: "1", - status: (() => { - if (data2.project_id === "1") return "tsubomi"; - if (data2.project_id === "7") return "hana"; - return "wakaba"; - })(), - description: data2.description, - location: data2.location, - sponsor: data4, - sponsor_data: { - target_amount_of_money: data3?.target_amount_of_money ?? 0, - location: data2.location, - motivation: data3?.motivation ?? undefined, - reports: data3?.reports ?? undefined, - fruits: data3?.fruits ?? undefined, - }, - seeds: data5.map((s) => ({ - category_id: s?.category_id ?? "", - created_at: s?.created_at ?? "", - description: s?.description ?? "", - location: s?.location ?? {}, - seed_id: s?.seed_id ?? "", - sower_id: s?.sower_id ?? "", - })), - }; - - type CompanyForm = { - description: string; - description_1000: string; - description_3000: string; - description_5000: string; - title_1000: string; - title_3000: string; - title_5000: string; - deadline: string; - location: string; - motivation: string; - amountOfMoney: number; - }; - const { - register, - handleSubmit, - reset, - formState: { errors }, - } = useForm(); - - const [formData, setFormData] = useState({ - description: "", - deadline: "", - location: "", - motivation: "", - amountOfMoney: 0, - title_1000: "", - title_3000: "", - title_5000: "", - description_1000: "", - description_3000: "", - description_5000: "", - }); - - let triggerDisable = true; - if ( - formData.description !== "" && - formData.deadline !== "" && - formData.location !== "" && - formData.motivation !== "" && - formData.amountOfMoney !== 0 && - formData.title_1000 !== "" && - formData.title_3000 !== "" && - formData.title_5000 !== "" && - formData.description_1000 !== "" && - formData.description_3000 !== "" && - formData.description_5000 !== "" - ) { - triggerDisable = false; - // eslint-disable-next-line no-console - console.log("トリガー確認"); - } - - const errorMessages = { - description: "プロジェクトの説明を入力してください", - deadline: "募集終了時期を入力してください", - location: "建築予定地を入力してください", - motivation: "モチベーションを入力してください", - title_1000: "タイトルを入力してください", - title_3000: "タイトルを入力してください", - title_5000: "タイトルを入力してください", - description_1000: "説明を入力してください", - description_3000: "説明を入力してください", - description_5000: "説明を入力してください", - amountOfMoney: "目標金額を入力してください", - }; - Object.entries(errors).forEach(([key, value]) => { - if (value != null || value !== undefined || value !== "" || value !== 0) { - toaster.error({ - id: key, - title: "エラー", - description: errorMessages[key as keyof typeof errorMessages], - }); - triggerDisable = true; - } - }); - - const onSubmit: SubmitHandler = (temp: CompanyForm) => { - setFormData(temp); - - // eslint-disable-next-line no-console - console.log(temp); - // eslint-disable-next-line no-console - console.log(formData); - }; - - return ( - -
{ - e.preventDefault(); - void handleSubmit(onSubmit)(e); - }} - > - - - - - {data.name} - - - - - - プロジェクトの説明 - - - ※ - - - - - - - - - 募集終了時期 - - - ※ - - - - - - - 📅 - - - Clear - - - - - - - - - - {(datePickers) => ( - - - - {datePickers.weekDays.map((weekDay) => ( - - {weekDay.short} - - ))} - - - - {datePickers.weeks.map((week) => ( - - {week.map((day) => ( - - - {day.day} - - - ))} - - ))} - - - )} - - - - - {(datePickers) => ( - - - {datePickers - .getMonthsGrid({ - columns: 4, - format: "short", - }) - .map((months) => ( - - {months.map((month) => ( - - - {month.label} - - - ))} - - ))} - - - )} - - - - - {(datePickers) => ( - - - {datePickers - .getYearsGrid({ columns: 4 }) - .map((years) => ( - - {years.map((year) => ( - - - {year.label} - - - ))} - - ))} - - - )} - - - - - - - - - - 建築予定地 - - - ※ - - - - - - - - - モチベーション - - - ※ - - - - - - - - - - 返礼品 - - - ※ - - - - - 1000円プラン - - - タイトル - - - - - 説明 - - - - - - - - - 3000円プラン - - - タイトル - - - - - 説明 - - - - - - - - - 5000円プラン - - - タイトル - - - - - 説明 - - - - - - - - - - - - 目標金額 - - - - - - 円 - - - - - - - - - - - - - - - - - - - - - {Object.entries(formData).some( - ([, value]) => - (typeof value === "string" && value === "") || - (typeof value === "number" && value === 0), - ) ? null : ( - - - 以下の内容に変更されます - - - - プロジェクトの説明 - {formData.description} - - - 募集終了時期 - {formData.deadline} - - - 建築予定地 - {formData.location} - - - モチベーション - {formData.motivation} - - - 1000円プラン - タイトル - {formData.title_1000} - 説明 - {formData.description_1000} - - - 3000円プラン - タイトル - {formData.title_3000} - 説明 - {formData.description_3000} - - - 5000円プラン - タイトル - {formData.title_5000} - 説明 - {formData.description_5000} - - - 目標金額 - {formData.amountOfMoney} - - - - - - - - - - - - )} - - - - - -
-
- ); - }, -}); +export const Route = createFileRoute('/company-form/$uuid')({ + component: () =>
Hello /company-form/$uuid!
, +}) diff --git a/src/routes/demo/-components/GeminiTest.tsx b/src/routes/demo/-components/GeminiTest.tsx index 4723fdf..ea08991 100644 --- a/src/routes/demo/-components/GeminiTest.tsx +++ b/src/routes/demo/-components/GeminiTest.tsx @@ -59,10 +59,12 @@ function Asking(): ReactElement { )) .otherwise(() => ( { - void result.mutate(); + containerProps={{ + onClick: () => { + void result.mutate(); + }, }} + icon="mdi:cloud-sync" > もう一度 Gemini に聞いてみる diff --git a/src/routes/demo/-components/ProjectCreation.tsx b/src/routes/demo/-components/ProjectCreation.tsx index 2c07811..95855e7 100644 --- a/src/routes/demo/-components/ProjectCreation.tsx +++ b/src/routes/demo/-components/ProjectCreation.tsx @@ -1,15 +1,18 @@ import { css } from "panda/css"; import { Flex, HStack, styled as p, VStack } from "panda/jsx"; -import { type ComponentProps, useState, type ReactElement } from "react"; +import { useState, type ReactElement, useMemo } from "react"; import useSWRImmutable from "swr/immutable"; import { match } from "ts-pattern"; import { IconText } from "@/components/IconText"; import { Button } from "@/components/cva/Button"; +import { GridLayout } from "@/components/cva/GridLayout"; import { ProjectCard } from "@/components/project/Card"; +import { Project } from "@/lib/classes/project"; import { ask } from "@/lib/services/ai"; import { fetchGeneratedImage } from "@/lib/services/image"; import { S } from "@/lib/utils/patterns"; import { notifyErrorInToast, toaster } from "@/lib/utils/toast"; +import { type TableBrandedId } from "@/types/table"; type Seed = { category: string; @@ -191,10 +194,12 @@ function Asking({ { - void swrProject.mutate(); + containerProps={{ + onClick: () => { + void swrProject.mutate(); + }, }} + icon="mdi:cloud-sync" > もう一度 Gemini に聞いてみる @@ -205,10 +210,12 @@ function Asking({ { - void swrProject.mutate(); + containerProps={{ + onClick: () => { + void swrProject.mutate(); + }, }} + icon="mdi:cloud-sync" > もう一度 Gemini に聞いてみる @@ -240,18 +247,26 @@ function SeedCard({ seed }: { seed: Seed }): ReactElement { export function ProjectCreation(): ReactElement { const [prompt, setPrompt] = useState(PROMPT); - const [project, setProject] = useState(); + const [projectData, setProjectData] = useState(); const [imageUrl, setImageUrl] = useState(); const [needAsk, setNeedAsk] = useState(false); - const projectCard = { - amount_of_money: 0, - key_visual: imageUrl ?? "https://placehold.jp/300x150.png", - location: { lon: 136.886326, lat: 35.172757 }, - name: project?.name ?? "", - status: "wakaba", - } satisfies ComponentProps; + const project = useMemo( + () => + new Project({ + project_id: + "08a78db3-f61b-4aac-bf7c-cad2fc45ebad" as TableBrandedId, + name: projectData?.name ?? "", + key_visual: imageUrl ?? "https://placehold.jp/300x150.png", + deadline: "2025-02-28 23:59:59+00", + created_at: "2024-10-30 16:22:50.279232+00", + territory_id: "dcc2053e-306d-4b2d-9704-47686925e626", + category_id: "55b46fee-ad75-4072-97b8-787ca0284c1d", + description: projectData?.description ?? "", + }), + [projectData, imageUrl], + ); return ( {needAsk ? ( - + ) : (