diff --git a/client/src/components/Footer.tsx b/client/src/components/Footer.tsx new file mode 100644 index 0000000..bc327a7 --- /dev/null +++ b/client/src/components/Footer.tsx @@ -0,0 +1,47 @@ +import { EXTERNAL_LINKS } from "../constants/links"; + +export default function Footer() { + return ( + + ); +} diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index d28d52b..42ce417 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -1,47 +1,76 @@ -import { HiOutlineHome, HiOutlineQuestionMarkCircle } from "react-icons/hi"; +import { useState } from "react"; +import { LuMenu, LuX } from "react-icons/lu"; import { NavLink } from "react-router"; +import { EXTERNAL_LINKS } from "../constants/links"; export default function Header() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + return ( -
-
- - logo - イツヒマ - (アルファ版) +
+
+ + イツヒマ + イツヒマ + + アルファ版 + -
-
-
- - - ホーム + + + +
+ -
-

イツヒマは現在アルファ版です。

- {/* biome-ignore lint/a11y/noNoninteractiveTabindex: daisyUI の仕様。tabIndex を消すとモバイルで開かないなどの問題が起こる */} - -
-
+ + {isMenuOpen && ( +
+
+ setIsMenuOpen(false)} + > + ホーム + + + 使い方ページ + + + ご意見・バグ報告 + +
+
+ )} +
); } diff --git a/client/src/constants/links.ts b/client/src/constants/links.ts new file mode 100644 index 0000000..8e84778 --- /dev/null +++ b/client/src/constants/links.ts @@ -0,0 +1,4 @@ +export const EXTERNAL_LINKS = { + GUIDE: "https://utcode.notion.site/1e4ca5f557bc80f2b697ca7b9342dc89?pvs=4", + FEEDBACK: "https://forms.gle/AB6xbgKjnDv5m1nm6", +} as const; diff --git a/client/src/index.css b/client/src/index.css index 4f1b301..505a373 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -15,7 +15,56 @@ } .btn { - font-weight: normal; + font-weight: bold; + @apply rounded-lg transition-all; +} + +.btn-primary { + @apply bg-primary text-white shadow-sm hover:bg-primary/90 hover:shadow-md; +} + +.btn-primary:disabled { + @apply bg-slate-300 text-slate-500 cursor-not-allowed opacity-60 hover:bg-slate-300 hover:shadow-sm; +} + +.btn-primary:focus-visible { + @apply outline-none ring-2 ring-primary ring-offset-2; +} + +.btn-primary:active:not(:disabled) { + @apply scale-95 shadow-none; +} + +.btn-outline { + @apply border-2 border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50; +} + +.btn-outline:disabled { + @apply border-slate-200 bg-slate-50 text-slate-400 cursor-not-allowed opacity-60 hover:border-slate-200 hover:bg-slate-50; +} + +.btn-outline:focus-visible { + @apply outline-none ring-2 ring-slate-400 ring-offset-2; +} + +.btn-outline:active:not(:disabled) { + @apply scale-95 bg-slate-100; +} + +.btn-ghost { + @apply bg-transparent hover:bg-slate-50; +} + +.btn-ghost:disabled { + @apply text-slate-400 cursor-not-allowed opacity-60 hover:bg-transparent; +} + +.btn-ghost:focus-visible { + @apply outline-none ring-2 ring-slate-300 ring-offset-2; +} + +.btn-ghost:active:not(:disabled) { + @apply scale-95 bg-slate-100; } :root { @@ -88,10 +137,6 @@ border: 1px solid var(--color-gray-200) !important; } -body { - background-color: var(--color-primary); -} - #root { background-color: white; } diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 0430380..a5acdc4 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -1,8 +1,9 @@ import { hc } from "hono/client"; import { useEffect, useState } from "react"; -import { HiOutlineCalendar, HiOutlinePlus, HiOutlineUser, HiOutlineUsers } from "react-icons/hi"; +import { LuCalendar, LuChevronRight, LuLayoutList, LuPlus, LuUser, LuUsers, LuX } from "react-icons/lu"; import { NavLink } from "react-router"; import type { AppType } from "../../../server/src/main"; +import Footer from "../components/Footer"; import Header from "../components/Header"; import { briefProjectReviver } from "../revivers"; import type { BriefProject } from "../types"; @@ -62,24 +63,36 @@ export default function HomePage() { return ( <> -
- {loading ? ( -
-
- +
+
+
+
+
+

ホーム

+

日程調整イベントの管理

+
+ + + 新規作成 +
-
- ) : involvedProjects ? ( - - ) : ( -
- -
- )} + {loading ? ( + + ) : involvedProjects && involvedProjects.length > 0 ? ( + + ) : ( + + )} + +
+
{toast && (
{toast.message} +
)} @@ -88,119 +101,115 @@ export default function HomePage() { } function ProjectDashboard({ involvedProjects }: { involvedProjects: BriefProject[] }) { - const sortedProjects = [...involvedProjects].sort((a, b) => { - if (a.isHost !== b.isHost) { - return a.isHost ? -1 : 1; - } - return new Date(b.startDate).getTime() - new Date(a.startDate).getTime(); - }); + const hostedProjects = involvedProjects + .filter((p) => p.isHost) + .sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()); + + const participatingProjects = involvedProjects + .filter((p) => !p.isHost) + .sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()); return ( -
-
- {/* Hero Section */} -
-
- logo -

イツヒマ

-
-

「いつヒマ?」で日程調整しよう

- - - 新しいイベントを作成 - -
+
+ {hostedProjects.length > 0 && ( + } projects={hostedProjects} /> + )} - {involvedProjects.length > 0 && ( -
- {/* All Projects */} -
-

- - あなたのイベント -

-
- {sortedProjects.map((project) => ( - - ))} -
-
-
- )} -
+ {participatingProjects.length > 0 && ( + } + projects={participatingProjects} + /> + )}
); } -function ProjectCard({ project }: { project: BriefProject }) { +function ProjectSection({ title, icon, projects }: { title: string; icon: React.ReactNode; projects: BriefProject[] }) { + return ( +
+
+ {icon} +

{title}

+ + {projects.length} + +
+
+ {projects.map((project, index) => ( + + ))} +
+
+ ); +} + +function ProjectRow({ project, isLast }: { project: BriefProject; isLast: boolean }) { + const formatDate = (date: Date) => { + return date.toLocaleDateString("ja-JP", { year: "numeric", month: "short", day: "numeric" }); + }; + return ( -
-
-

{project.name}

- - {project.isHost ? ( - <> - - 主催者 - - ) : ( - <> - - 参加者 - - )} - -
- -
- - - {formatDate(project.startDate.toLocaleDateString())} ~{formatDate(project.endDate.toLocaleDateString())} +
+

+ {project.name} +

+
+ + + {formatDate(project.startDate)} 〜 {formatDate(project.endDate)}
- - + ); } function EmptyState() { return ( -
-
-
- -
-

まだイベントがありません

-

イベントを作成して、日程調整を始めましょう

+
+
+
- - - イベントを作成する - +

まだイベントがありません

+

「新規作成」ボタンから、新しい日程調整を始めましょう。

); } -// ---------- Utility ---------- -const formatDate = (isoDate: string) => { - const date = new Date(isoDate); - return date.toLocaleDateString("ja-JP"); -}; +function ProjectsSkeleton() { + return ( +
+ {[1, 2].map((section) => ( +
+
+
+
+
+
+ {[1, 2, 3].map((row, index) => ( +
+
+
+
+
+
+
+ ))} +
+
+ ))} +
+ ); +} diff --git a/client/src/pages/Landing.tsx b/client/src/pages/Landing.tsx index 814d42f..88559ec 100644 --- a/client/src/pages/Landing.tsx +++ b/client/src/pages/Landing.tsx @@ -1,38 +1,32 @@ +import { LuChevronRight, LuCircleCheck, LuPalette, LuSmartphone } from "react-icons/lu"; import { NavLink } from "react-router"; +import Footer from "../components/Footer"; import Header from "../components/Header"; -export default function LandingPage() { - return ( - <> -
-
- - -
-
- - ); -} - function HeroSection() { return ( -
-
-
-

- 「いつヒマ?」
- 日程調整しよう -

-

- とりあえずみんなの空いている時間を訊いてから、何を何時間やるか決めたい。そんな仲間うちでの日程調整に最適なツールです。 -

- - 今すぐイベントを作成 - -
-
-
- イツヒマアプリの画面 +
+
+
+
+
+

+ 「いつヒマ?」
+ 日程調整しよう +

+

+ とりあえずみんなの空いている時間を訊いてから、何を何時間やるか決めたい。そんな仲間うちでの調整に最適な、シンプルで直感的なツールです。 +

+ + イベントを作成 + + +
+ +
+ App Screenshot +
+
@@ -42,90 +36,63 @@ function HeroSection() { function FeaturesSection() { const features = [ { - icon: "🚫", - title: "候補日程の設定なし", - description: "みんなが空いている時間を選ぶだけなので、主催者が候補日程を設定する必要がありません。", + icon: , + title: "候補日程は不要", + description: + "参加者が各々の空いている日程を入力することで日程調整を行います。幹事が候補日程を大量に作成する必要はありません。", + color: "bg-emerald-50", }, { - icon: "🔗", - title: "URLで簡単共有", - description: "作成したイベントのURLをコピーして友達に送れば、すぐに日程調整が可能です。", + icon: , + title: "複数の参加形態に対応", + description: "「対面」「オンライン」など、複数の参加形態を自由に設定可能です。", + color: "bg-primary/10", }, { - icon: "📱", - title: "直感的な操作", - description: "複数日程も一気にドラッグで選択可能。スマホでも簡単に操作できます。", + icon: , + title: "スマホでも簡単に入力可能", + description: "スマホでも、複数の日程をドラッグで一気に選択可能です。", + color: "bg-blue-50", }, - ]; + ] as const; return ( -
-
-
-

イツヒマの特長

-

仲間うちでのスムーズな日程調整に特化

-
- -
- {features.map((feature) => ( -
-
{feature.icon}
-

{feature.title}

-

{feature.description}

-
- ))} +
+
+
+
+

イツヒマの特徴

+

イツヒマは仲間うちでのスムーズな日程調整に特化しています。

+
+
+ {features.map((feature) => ( +
+
+ {feature.icon} +
+

{feature.title}

+

{feature.description}

+
+ ))} +
); } -function Footer() { +export default function LandingPage() { return ( -
-
-
-
-

イツヒマについて

-

- イツヒマは、「いつヒマ?」で日程調整できるツールです。 -
- 候補日程の設定なしで、仲間うちでの日程調整をスムーズに行うことができます。 -

-
- -
-
-

© 2024 イツヒマ (アルファ版)

-
-
-
+
+
+
+ + +
+
+
); } diff --git a/client/src/pages/NotFound.tsx b/client/src/pages/NotFound.tsx index b52bc62..ca8e081 100644 --- a/client/src/pages/NotFound.tsx +++ b/client/src/pages/NotFound.tsx @@ -1,16 +1,27 @@ +import { LuCircleAlert, LuHouse } from "react-icons/lu"; import { NavLink } from "react-router"; import Header from "../components/Header"; export default function NotFoundPage() { return ( -
+
-
-

このページは見つかりませんでした。

- - ホームに戻る - -
+
+
+
+ +
+

404

+

ページが見つかりません

+

+ お探しのページは存在しないか、移動または削除された可能性があります。 +

+ + + ホームに戻る + +
+
); } diff --git a/client/src/pages/Project.tsx b/client/src/pages/Project.tsx index b7230a4..bc1e21a 100644 --- a/client/src/pages/Project.tsx +++ b/client/src/pages/Project.tsx @@ -4,19 +4,24 @@ import { hc } from "hono/client"; import { useCallback, useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { - HiClipboardCheck, - HiClipboardCopy, - HiInformationCircle, - HiOutlineCheckCircle, - HiOutlineExclamationCircle, - HiOutlineTrash, -} from "react-icons/hi"; + LuChevronLeft, + LuChevronRight, + LuCircleAlert, + LuCircleCheck, + LuClipboard, + LuClipboardCheck, + LuInfo, + LuPlus, + LuTrash2, + LuX, +} from "react-icons/lu"; import { NavLink, useNavigate, useParams } from "react-router"; import type { z } from "zod"; import { DEFAULT_PARTICIPATION_OPTION, generateDistinctColor } from "../../../common/colors"; import { editReqSchema, projectReqSchema } from "../../../common/validators"; import type { AppType } from "../../../server/src/main"; import Header from "../components/Header"; +import { EXTERNAL_LINKS } from "../constants/links"; import { projectReviver } from "../revivers"; import type { Project } from "../types"; import { API_ENDPOINT, FRONTEND_ORIGIN } from "../utils"; @@ -271,122 +276,113 @@ export default function ProjectPage() { return ( <> -
+
{loading ? ( -
- +
+
) : eventId !== undefined && !project ? ( -
-

イベントが見つかりませんでした。

- - ホームに戻る - +
+
+

イベントが見つかりませんでした。

+ + ホームに戻る + +
) : ( -
-

- {project ? `${project.name} の編集` : "イベントの作成"} -

-
-
- - trigger("name")} - /> - {errors.name &&

{errors.name.message}

} -
-
- -