diff --git a/backend/src/routes/calendarItem.ts b/backend/src/routes/calendarItem.ts index 552eb8a..ae8efd8 100644 --- a/backend/src/routes/calendarItem.ts +++ b/backend/src/routes/calendarItem.ts @@ -468,6 +468,7 @@ app.openapi(createCalendarItemRoute, async (c) => { rotation: stringifyCoordinates(calendarItem.rotation), itemId: calendarItem.itemId, imageId: calendarItem.imageId, + isOpened: calendarItem.isOpened ?? false, }) .returning(); diff --git a/bun.lock b/bun.lock index 6f669cd..6a74961 100644 --- a/bun.lock +++ b/bun.lock @@ -51,6 +51,7 @@ "name": "frontend", "dependencies": { "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", @@ -446,6 +447,8 @@ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -716,7 +719,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -1894,6 +1897,8 @@ "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "@types/three/@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/frontend/package.json b/frontend/package.json index eed761e..2d8f5a3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", @@ -35,6 +36,7 @@ "common": "workspace:*", "date-fns": "^4.1.0", "lucide-react": "^0.544.0", + "nanoid": "^5.1.6", "react": "^19.2.0", "react-day-picker": "^9.12.0", "react-dom": "^19.2.0", @@ -43,7 +45,6 @@ "tailwindcss": "^4.0.6", "three": "^0.182.0", "tw-animate-css": "^1.3.6", - "nanoid": "^5.1.6", "zod": "^4.1.13" }, "devDependencies": { diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..39d9192 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { CheckIcon } from "lucide-react"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/frontend/src/features/edit/hooks/useGetInfiniteItems.ts b/frontend/src/features/edit/hooks/useGetInfiniteItems.ts index ac5a15d..380d8f0 100644 --- a/frontend/src/features/edit/hooks/useGetInfiniteItems.ts +++ b/frontend/src/features/edit/hooks/useGetInfiniteItems.ts @@ -8,7 +8,7 @@ export const useGetInfiniteItems = (type: string) => { queryFn: ({ pageParam }) => getItems({ offset: pageParam, - limit: 30, + limit: 60, type: type === "all" ? undefined : type, }), initialPageParam: 0, diff --git a/frontend/src/routes/new.tsx b/frontend/src/routes/new.tsx index b7a89c5..3a374e9 100644 --- a/frontend/src/routes/new.tsx +++ b/frontend/src/routes/new.tsx @@ -1,5 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { createFileRoute, Link } from "@tanstack/react-router"; +import { usePostCalendarItemsRoomIdCalendarItems } from "common/generate/calendar-items/calendar-items"; import { usePostRooms } from "common/generate/room/room"; import { format } from "date-fns"; import { @@ -8,17 +9,18 @@ import { Check, Copy, Gift, + Handbag, Info, LinkIcon, Lock, UsersRound, } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -// import { Link } from "@tanstack/react-router"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; +import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { @@ -28,7 +30,9 @@ import { } from "@/components/ui/popover"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; +import { R2_BASE_URL } from "@/constants/r2-url"; import { useUser } from "@/context/UserContext"; +import { useGetInfiniteItems } from "@/features/edit/hooks/useGetInfiniteItems"; import NameInput from "@/features/user/nameInput"; import { cn } from "@/lib/utils"; @@ -42,10 +46,10 @@ const formSchema = z item_get_time: z.union([z.string(), z.date()]).optional(), password: z.string(), is_anonymous: z.union([z.boolean(), z.literal("")]), + default_item_ids: z.array(z.string()), }) .refine( (data) => { - // start_atが空文字でないことを確認 if (typeof data.start_at === "string" && data.start_at === "") { return false; } @@ -74,9 +78,53 @@ function RouteComponent() { link: false, password: false, }); + // クリスマス・その他の持ち物リスト開閉状態 + const [openChristmas, setOpenChristmas] = useState(false); + const [openAllSeason, setOpenAllSeason] = useState(false); const { mutateAsync: postRooms } = usePostRooms(); + const { mutateAsync: postCalendarItem } = + usePostCalendarItemsRoomIdCalendarItems(); const { user } = useUser(); + // アイテム一覧の取得 + const { data: itemsData, isLoading: isLoadingItems } = + useGetInfiniteItems("all"); + const items = itemsData?.pages.flat() ?? []; + + // アイテムをカテゴリーごとに分類 + const groupedItems = useMemo(() => { + // フォトフレーム + const photoFrame = items.filter((item) => { + const type = item.type; + if (!type) return false; + if (typeof type === "string") return type.includes("photo_frame"); + if (Array.isArray(type)) + return (type as string[]).includes("photo_frame"); + return false; + }); + // クリスマス + const christmas = items.filter((item) => { + const type = item.type; + if (!type) return false; + if (typeof type === "string") return type.includes("christmas"); + if (Array.isArray(type)) return (type as string[]).includes("christmas"); + return false; + }); + // フォトフレームとクリスマス以外を「オールシーズン」として扱う + const allSeason = items.filter((item) => { + const type = item.type; + if (!type) return true; + const isPhotoFrame = + (typeof type === "string" && type.includes("photo_frame")) || + (Array.isArray(type) && type.includes("photo_frame")); + const isChristmas = + (typeof type === "string" && type.includes("christmas")) || + (Array.isArray(type) && type.includes("christmas")); + return !(isPhotoFrame || isChristmas); + }); + return { photoFrame, allSeason, christmas }; + }, [items]); + const { register, handleSubmit, @@ -86,10 +134,11 @@ function RouteComponent() { } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - start_at: new Date(2025, 11, 1), // 2025年12月1日 + start_at: new Date(2025, 11, 1), item_get_time: "", password: "", is_anonymous: true, + default_item_ids: [], }, }); @@ -102,7 +151,6 @@ function RouteComponent() { throw new Error("ユーザーが存在しません"); } - // APIに送信するデータ形式に変換 const apiData = { ownerId: user.id, startAt: @@ -117,13 +165,38 @@ function RouteComponent() { : data.item_get_time, password: data.password || undefined, isAnonymous: !data.is_anonymous as boolean, + // バックエンドによる自動作成を防ぐため空配列を渡す + defaultItemIds: [], }; console.log("API送信データ:", apiData); - // 実際のAPI呼び出し + // 1. ルーム作成(アイテムなしで作成) const response = await postRooms({ data: apiData }); + // 2. 持ち物(初期インベントリー)をフロントエンド主導で一括登録 + if (response.id && response.editId && data.default_item_ids.length > 0) { + await Promise.all( + data.default_item_ids.map((itemId) => + postCalendarItem({ + roomId: response.id, + data: { + editId: response.editId, + calendarItem: { + userId: user.id, + roomId: response.id, + openDate: apiData.startAt, + itemId, + isOpened: true, + position: null, + rotation: null, + }, + }, + }), + ), + ); + } + setSuccessData({ id: response.id, editId: response.editId, @@ -142,13 +215,11 @@ function RouteComponent() { const copyToClipboard = (text: string, type: "link" | "password") => { navigator.clipboard.writeText(text); setCopiedStates((prev) => ({ ...prev, [type]: true })); - // 2秒後にアイコンを元に戻す setTimeout(() => { setCopiedStates((prev) => ({ ...prev, [type]: false })); }, 2000); }; - // 日付の表示用計算 const displayDate = () => { if (typeof watchedValues.start_at === "object" && watchedValues.start_at) { const startDate = watchedValues.start_at; @@ -162,11 +233,9 @@ function RouteComponent() { return null; }; - // アイテム取得時間の計算 const isRandomTime = !watchedValues.item_get_time || watchedValues.item_get_time === ""; - // ユーザーが存在しない場合は名前入力フォームを表示 if (!user) { return ; } @@ -258,216 +327,413 @@ function RouteComponent() { } return ( -
-
-

- アドベントカレンダーを作ろう! -

-
-
- -

- 期間(25日間固定) -

-
- - - - - - { - if (selectedDate) { - setValue("start_at", selectedDate); - } - }} - numberOfMonths={2} - /> - - - {errors.start_at && ( -

{errors.start_at.message}

- )} + +

+ アドベントカレンダーを作ろう! +

+ + {/* --- 期間選択 --- */} +
+
+ +

+ 期間(25日間固定) +

-
-
- -

- アイテムゲット時間 -

-
- { - if (value === "random") { - setValue("item_get_time", ""); - } else { - setValue("item_get_time", new Date(`2024-01-01T10:30:00`)); + + + + + + -