diff --git a/src/components/sva/datePicker.ts b/src/components/sva/datePicker.ts new file mode 100644 index 0000000..47a5ab6 --- /dev/null +++ b/src/components/sva/datePicker.ts @@ -0,0 +1,127 @@ +import { datePickerAnatomy } from "@ark-ui/react"; +import { sva } from "panda/css"; + +export const svaDatePicker = sva({ + className: "datePicker", + slots: datePickerAnatomy.keys(), + base: { + control: { + display: "flex", + alignItems: "center", + gap: "8px", + padding: "8px", + borderRadius: "md", + backgroundColor: "wkb.bg", + shadow: "md", + _hover: { + borderColor: "gray.400", + }, + _focusWithin: { + borderColor: "blue.500", + }, + }, + input: { + fontSize: "16px", + color: "gray.700", + bg: "wkb.bg", + padding: "0 8px", + width: "100%", + _focus: { + outline: "none", + }, + }, + trigger: { + fontSize: "20px", + color: "gray.500", + cursor: "pointer", + _hover: { + color: "blue.500", + }, + }, + clearTrigger: { + fontSize: "20px", + color: "gray.500", + cursor: "pointer", + _hover: { + color: "red.500", + }, + }, + content: { + padding: "16px", + border: "1px solid", + borderColor: "gray.300", + borderRadius: "md", + boxShadow: "lg", + backgroundColor: "white", + }, + positioner: { + zIndex: 10, + }, + yearSelect: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "8px 16px", + }, + monthSelect: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "8px 16px", + }, + table: { + width: "100%", + borderCollapse: "collapse", + marginTop: "8px", + }, + tableRow: { + display: "flex", + justifyContent: "space-between", + width: "100%", + }, + tableCell: { + width: "14.285%", + height: "36px", + textAlign: "center", + verticalAlign: "middle", + cursor: "pointer", + _hover: { + backgroundColor: "gray.100", + }, + _selected: { + backgroundColor: "blue.500", + color: "white", + }, + }, + tableHead: { + fontWeight: "bold", + color: "gray.500", + display: "flex", + width: "1%", + }, + prevTrigger: { + display: "flex", + alignItems: "center", + backgroundColor: "blue.500", + color: "white", + padding: "4px 8px", + borderRadius: "4px", + cursor: "pointer", + _hover: { + backgroundColor: "blue.400", + }, + }, + nextTrigger: { + display: "flex", + alignItems: "center", + backgroundColor: "blue.500", + color: "white", + padding: "4px 8px", + borderRadius: "4px", + cursor: "pointer", + _hover: { + backgroundColor: "blue.400", + }, + }, + }, +}); diff --git a/src/components/sva/formDialog.ts b/src/components/sva/formDialog.ts new file mode 100644 index 0000000..734d3a9 --- /dev/null +++ b/src/components/sva/formDialog.ts @@ -0,0 +1,60 @@ +import { dialogAnatomy } from "@ark-ui/react"; +import { sva } from "panda/css"; + +/** + * Ark UI の [Popover コンポーネント](https://ark-ui.com/react/docs/components/popover) の基底スタイル + * + * 書き方 refs: + * - https://panda-css.com/docs/concepts/slot-recipes + * - https://ark-ui.com/react/docs/guides/styling#styling-with-panda-css + * - https://speakerdeck.com/ikumatadokoro/panda-css-to-ark-ui-dehazimeruge-ren-kai-fa?slide=31 + */ +export const svaFormDialog = sva({ + className: "formDialog", + slots: dialogAnatomy.keys(), + base: { + content: { + backgroundColor: "wkb-neutral.0", + borderRadius: "md", + boxShadow: "md", + padding: "md", + position: "fixed", + zIndex: "modalContent", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + rounded: "md", + // fade in animation + animation: "fadeIn 0.3s", + p: 4, + }, + title: { + fontSize: "lg", + fontWeight: "bold", + marginBottom: "md", + mb: 4, + }, + description: { + marginBottom: "md", + fontSize: "md", + }, + trigger: { + cursor: "pointer", + }, + backdrop: { + animation: "fadeIn 0.3s", + backgroundColor: "wkb-neutral.0/20", + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: "modal", + }, + closeTrigger: { + right: "sm", + top: "sm", + color: "wkb.primary", + }, + }, +}); diff --git a/src/components/sva/numberInput.ts b/src/components/sva/numberInput.ts new file mode 100644 index 0000000..9d56fc5 --- /dev/null +++ b/src/components/sva/numberInput.ts @@ -0,0 +1,32 @@ +import { numberInputAnatomy } from "@ark-ui/react"; +import { sva } from "panda/css"; + +export const svaNumberInput = sva({ + className: "numberInput", + slots: numberInputAnatomy.keys(), + base: { + root: { + display: "flex", + alignItems: "center", + borderRadius: "md", + backgroundColor: "wkb.bg", + textAlign: "right", + width: "100%", + _hover: { + borderColor: "gray.400", + }, + _focusWithin: { + borderColor: "blue.500", + }, + }, + input: { + fontSize: "6xl", + color: "gray.700", + textAlign: "right", + width: "100%", + _focus: { + outline: "none", + }, + }, + }, +}); diff --git a/src/components/sva/textArea.ts b/src/components/sva/textArea.ts new file mode 100644 index 0000000..55272d9 --- /dev/null +++ b/src/components/sva/textArea.ts @@ -0,0 +1,60 @@ +import { fieldAnatomy } from "@ark-ui/react"; +import { sva } from "panda/css"; + +/** + * Ark UI の [Popover コンポーネント](https://ark-ui.com/react/docs/components/popover) の基底スタイル + * + * 書き方 refs: + * - https://panda-css.com/docs/concepts/slot-recipes + * - https://ark-ui.com/react/docs/guides/styling#styling-with-panda-css + * - https://speakerdeck.com/ikumatadokoro/panda-css-to-ark-ui-dehazimeruge-ren-kai-fa?slide=31 + */ +export const svaTextArea = sva({ + className: "TextArea", + slots: fieldAnatomy.keys(), + base: { + root: { + display: "grid", + gap: "0.5rem", + gridTemplateColumns: "1fr", + width: "100%", + margin: "auto", + borderRadius: "md", + }, + textarea: { + bg: "wkb.bg", + rounded: "md", + shadow: "md", + duration: "200ms", + width: "100%", + minHeight: "200px", + outline: "none", + p: "1rem", + _focus: { + outline: "solid 2px wkb.primary", + }, + }, + select: { + bg: "wkb.bg", + border: "1px solid", + borderColor: "wkb.bg", + rounded: "md", + shadow: "md", + duration: "200ms", + }, + input: { + minHeight: "48px", + bg: "wkb.bg", + rounded: "md", + shadow: "md", + width: "100%", + p: "1rem", + _open: { + // + }, + _closed: { + // + }, + }, + }, +}); diff --git a/src/routes/company-form/$uuid.tsx b/src/routes/company-form/$uuid.tsx new file mode 100644 index 0000000..49ee3e1 --- /dev/null +++ b/src/routes/company-form/$uuid.tsx @@ -0,0 +1,776 @@ +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 { + 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} + + + + + + + + + + + + )} + + + + + +
+
+ ); + }, +});