diff --git a/bun.lockb b/bun.lockb
index db6fe8a..7361756 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 0c79e6e..4cc8657 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"dependencies": {
"@fontsource-variable/noto-sans-jp": "^5.0.19",
"@generouted/react-router": "^1.19.3",
+ "@hookform/resolvers": "^3.9.0",
"@iconify/react": "^4.1.1",
"@nanostores/persistent": "^0.10.1",
"@nanostores/react": "^0.7.2",
@@ -66,6 +67,6 @@
"typescript": "^5.4.5",
"vite": "^5.2.0",
"vite-tsconfig-paths": "^4.3.2",
- "wrangler": "^3.61.0"
+ "wrangler": "^3.73.0"
}
}
diff --git a/src/components/achievements/Card.tsx b/src/components/achievements/Card.tsx
index 6be41ab..9ceacc4 100644
--- a/src/components/achievements/Card.tsx
+++ b/src/components/achievements/Card.tsx
@@ -86,8 +86,12 @@ export function AchievementCard({
- #{achievement.tags[0].name}
- #{achievement.tags[0].name}
+ {achievement.tags.map((t, idx) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+ #{t}
+
+ ))}
);
}
diff --git a/src/components/achievements/UnlockableCard.tsx b/src/components/achievements/UnlockableCard.tsx
index 6ffa85b..91bc864 100644
--- a/src/components/achievements/UnlockableCard.tsx
+++ b/src/components/achievements/UnlockableCard.tsx
@@ -98,8 +98,8 @@ export function UnlockableCard({
- #{achievement.tags[0].name}
- #{achievement.tags[0].name}
+ #{achievement.tags}
+ #{achievement.tags}
);
}
diff --git a/src/lib/consts.ts b/src/lib/consts.ts
index e68d6fc..c35a3fc 100644
--- a/src/lib/consts.ts
+++ b/src/lib/consts.ts
@@ -34,4 +34,4 @@ export function getLocalStorageKey(key: string, trailingColon = false): string {
return `${APP_NAME}.v${LOCAL_STORAGE_VERSION}.${key}${trailingColon ? ":" : ""}`;
}
-export const DB_VERSION = "1";
+export const DB_VERSION = "2";
diff --git a/src/pages/achievements/[id]/index.tsx b/src/pages/achievements/[id]/index.tsx
index c420353..2da42ba 100644
--- a/src/pages/achievements/[id]/index.tsx
+++ b/src/pages/achievements/[id]/index.tsx
@@ -27,7 +27,7 @@ export default function Page(): ReactElement {
icon: {d.icon}
createdAt: {String(d.createdAt)}
updatedAt: {String(d.updatedAt)}
- tags: {d.tags.map((tag) => tag.name).join(", ")}
+ tags: {d.tags.join(", ")}
);
}
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index 1f72a0d..7b67420 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -1,5 +1,6 @@
/* eslint-disable react-refresh/only-export-components */
+import { yupResolver } from "@hookform/resolvers/yup";
import { Icon } from "@iconify/react";
import {
TextField,
@@ -11,9 +12,13 @@ import {
Popover,
} from "@radix-ui/themes";
import { useState, type ReactElement } from "react";
-import { useForm, type SubmitHandler } from "react-hook-form";
+import { type SubmitHandler, useForm } from "react-hook-form";
import styled from "styled-components";
-import { type Achievement } from "@/types/post-data/achievements";
+import { match } from "ts-pattern";
+import { useAchievements } from "@/hooks/db/achievements";
+import { useTeam } from "@/hooks/teams";
+import yup from "@/lib/yup-locate";
+import { type Achievement, yAchievement } from "@/types/post-data/achievements";
const FormStyle = styled(Flex)`
margin-top: 4rem;
@@ -34,7 +39,7 @@ const AvatarStyle = styled(Avatar)`
border: 10px solid #e7e7e7;
`;
-const Button1 = styled(Box)`
+const SubmitButton = styled.input`
font-weight: 600;
font-family: sans-serif;
font-size: 1rem;
@@ -57,6 +62,7 @@ const Button1 = styled(Box)`
overflow: hidden;
position: relative;
z-index: 1;
+ cursor: pointer;
box-shadow:
6px 6px 16px #b5bec9,
@@ -92,6 +98,15 @@ const Button1 = styled(Box)`
transform: scaleX(100%);
transform: none;
}
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.3;
+ color: #ffffff;
+ background-color: #00cdc2;
+ transform: scale(1.06);
+ box-shadow: none;
+ }
`;
const PlusButton = styled(IconButton)`
@@ -108,7 +123,7 @@ const PlusButton = styled(IconButton)`
}
`;
-const ImputStyle = styled(TextField.Root)`
+const TextInput = styled(TextField.Root)<{ invalid: number }>`
position: relative;
background-color: #e7e7e7;
margin-top: 0.6rem;
@@ -119,50 +134,120 @@ const ImputStyle = styled(TextField.Root)`
margin-left: 0.4rem;
color: #737a89;
}
+ border: 2px solid ${({ invalid }) => (invalid !== 0 ? "#e03b3b" : "unset")};
`;
-export default function create(): ReactElement {
- const [selectIcon, setSelectIcon] = useState(
- "https://api.iconify.design/twemoji:trophy.svg",
- );
+const MessageContainer = styled(Flex)`
+ justify-content: center;
+ align-items: center;
+ font-size: 0.8rem;
+ gap: 10px;
+`;
- const { register, handleSubmit, setValue } = useForm({
- mode: "onSubmit",
+const ErrorMessageContainer = styled(MessageContainer)`
+ color: #e03b3b;
+`;
+
+const SuccessMessageContainer = styled(MessageContainer)`
+ color: #00cdc2;
+`;
+
+const ErrorMessage = styled(Text)`
+ color: #e03b3b;
+ font-size: 0.8rem;
+`;
+
+const yAchievementForm = yAchievement.concat(
+ yup.object({
+ id: yup.mixed().notRequired(),
+ createdAt: yup.mixed().notRequired(),
+ updatedAt: yup.mixed().notRequired(),
+ }),
+);
+
+const ICON_URLS = [
+ "https://api.iconify.design/twemoji:trophy.svg",
+ "https://api.iconify.design/twemoji:meat-on-bone.svg",
+ "https://api.iconify.design/twemoji:horse-racing-medium-skin-tone.svg",
+ "https://api.iconify.design/twemoji:steaming-bowl.svg",
+ "https://api.iconify.design/twemoji:shopping-cart.svg",
+ "https://api.iconify.design/twemoji:page-facing-up.svg",
+ "https://api.iconify.design/twemoji:laptop.svg",
+ "https://api.iconify.design/twemoji:zombie.svg",
+ "https://api.iconify.design/twemoji:face-with-symbols-on-mouth.svg",
+ "https://api.iconify.design/twemoji:broken-heart.svg",
+ "https://api.iconify.design/twemoji:fire.svg",
+ "https://api.iconify.design/twemoji:beer-mug.svg",
+ "https://api.iconify.design/twemoji:beetle.svg",
+ "https://api.iconify.design/twemoji:bathtub.svg",
+ "https://api.iconify.design/twemoji:broccoli.svg",
+ "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgUqLcDsoa_vMqHK_IPFR4GoMT9RYnH6gtzw9nqHl2AfJeQI7Bm6vd2LphkvWznofSU0yXGcFCWEmO1owcCDJKqaijH4sDyK6r7gwjHUoqD-lVYxHPO9m6khg559gSY2FVv9qia_dHQPxbQ/s800/school_tani_otosu_boy.png",
+ "https://qr.paps.jp/8o3Og",
+ "https://i.imgur.com/5TaVIlf.gif",
+ "https://qr.paps.jp/fblo0",
+ "https://i.gifer.com/9ZNS.gif",
+] as const;
+
+export default function Page(): ReactElement {
+ const {
+ register,
+ handleSubmit,
+ setValue,
+ getValues,
+ formState,
+ setError,
+ reset,
+ } = useForm({
+ mode: "onBlur",
+ reValidateMode: "onChange",
+ resolver: yupResolver(yAchievementForm),
+ defaultValues: {
+ icon: ICON_URLS[0],
+ },
});
- const onSubmit: SubmitHandler = (data) => {
- // eslint-disable-next-line no-console
- console.log(data);
- };
+ const { errors, isSubmitting } = formState;
+ const { fetch, update } = useAchievements(useTeam);
+
+ const [isPopoverOpened, setPopoverOpened] = useState(false);
- const iconUrl = [
- "https://api.iconify.design/twemoji:trophy.svg",
- "https://api.iconify.design/twemoji:meat-on-bone.svg",
- "https://api.iconify.design/twemoji:horse-racing-medium-skin-tone.svg",
- "https://api.iconify.design/twemoji:steaming-bowl.svg",
- "https://api.iconify.design/twemoji:shopping-cart.svg",
- "https://api.iconify.design/twemoji:page-facing-up.svg",
- "https://api.iconify.design/twemoji:laptop.svg",
- "https://api.iconify.design/twemoji:zombie.svg",
- "https://api.iconify.design/twemoji:face-with-symbols-on-mouth.svg",
- "https://api.iconify.design/twemoji:broken-heart.svg",
- "https://api.iconify.design/twemoji:fire.svg",
- "https://api.iconify.design/twemoji:beer-mug.svg",
- "https://api.iconify.design/twemoji:beetle.svg",
- "https://api.iconify.design/twemoji:bathtub.svg",
- "https://api.iconify.design/twemoji:broccoli.svg",
- "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgUqLcDsoa_vMqHK_IPFR4GoMT9RYnH6gtzw9nqHl2AfJeQI7Bm6vd2LphkvWznofSU0yXGcFCWEmO1owcCDJKqaijH4sDyK6r7gwjHUoqD-lVYxHPO9m6khg559gSY2FVv9qia_dHQPxbQ/s800/school_tani_otosu_boy.png",
- "https://qr.paps.jp/8o3Og",
- "https://i.imgur.com/5TaVIlf.gif",
- "https://qr.paps.jp/fblo0",
- "https://i.gifer.com/9ZNS.gif",
- ];
+ const onSubmit: SubmitHandler<
+ yup.InferType
+ > = async (data) => {
+ try {
+ const achievements = await fetch();
+ if (achievements == null) {
+ throw new Error(
+ "`achievements` is null! Maybe you forgot to call `init()`",
+ );
+ }
+
+ const achievement: Achievement = {
+ ...data,
+ tags: data.tags.filter((tag) => tag !== ""),
+ id: achievements.length + 1,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ await update([...achievements, achievement]);
+ reset();
+ } catch (e) {
+ setError("root.submit", { message: String(e) });
+ throw e;
+ }
+ };
return (
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
-
);
diff --git a/src/types/post-data/achievements.ts b/src/types/post-data/achievements.ts
index 2d23a45..d6cdb49 100644
--- a/src/types/post-data/achievements.ts
+++ b/src/types/post-data/achievements.ts
@@ -6,16 +6,7 @@ export const yAchievement = yup.object().shape({
name: yup.string().required(),
description: yup.string().required(),
icon: yup.string().required(),
- tags: yup
- .array()
- .of(
- yup.object().shape({
- id: yup.number().required(),
- name: yup.string().required(),
- color: yup.string().required(),
- }),
- )
- .required(),
+ tags: yup.array().of(yup.string()).required(),
createdAt: yup.date().required(),
updatedAt: yup.date().required(),
});