diff --git a/client/index.html b/client/index.html index 7384260..2f54bc6 100644 --- a/client/index.html +++ b/client/index.html @@ -3,13 +3,13 @@ - イツヒマ + イツヒマ - 「いつヒマ?」で日程調整しよう - - - + + + diff --git a/client/public/mock-mobile.png b/client/public/mock-mobile.png new file mode 100644 index 0000000..8dcab22 Binary files /dev/null and b/client/public/mock-mobile.png differ diff --git a/client/public/og-image.jpg b/client/public/og-image.jpg new file mode 100644 index 0000000..184535d Binary files /dev/null and b/client/public/og-image.jpg differ diff --git a/client/public/og-image.webp b/client/public/og-image.webp deleted file mode 100644 index 5b74fc1..0000000 Binary files a/client/public/og-image.webp and /dev/null differ diff --git a/client/src/App.tsx b/client/src/App.tsx index 8b283f7..0dc6179 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,14 +1,16 @@ import { BrowserRouter, Outlet, Route, Routes } from "react-router"; +import HomePage from "./pages/Home.tsx"; +import LandingPage from "./pages/Landing.tsx"; import NotFoundPage from "./pages/NotFound.tsx"; import ProjectPage from "./pages/Project.tsx"; -import RootPage from "./pages/Root.tsx"; import SubmissionPage from "./pages/eventId/Submission.tsx"; export default function App() { return ( - } /> + } /> + } /> } /> }> } /> diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 0209bc4..c4f170a 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -5,7 +5,7 @@ export default function Header() { return (
- + logo イツヒマ (アルファ版) diff --git a/client/src/hooks.ts b/client/src/hooks.ts index 346de1e..6f68f9c 100644 --- a/client/src/hooks.ts +++ b/client/src/hooks.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import type { ZodType, ZodTypeDef } from "zod"; +import { API_ENDPOINT } from "./utils"; export function useData( url: string | null, @@ -48,3 +49,42 @@ export function useData( return { data, loading, error, refetch: fetchData }; } + +export function useAuth(): { isAuthenticated: boolean | null } { + const [isAuthenticated, setIsAuthenticated] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + + const checkAuth = async () => { + try { + const res = await fetch(`${API_ENDPOINT}/projects/mine`, { + method: "GET", + credentials: "include", + signal: controller.signal, + }); + + if (res.ok) { + setIsAuthenticated(true); + } else if (res.status === 401 || res.status === 403) { + setIsAuthenticated(false); + } else { + setIsAuthenticated(false); + console.error(`Unexpected response: ${res.status} ${res.statusText}`); + } + } catch (err) { + if (err instanceof DOMException && err.name === "AbortError") { + return; + } + setIsAuthenticated(false); + console.error("Auth check failed:", err); + } + }; + + checkAuth(); + + return () => controller.abort(); + }, []); + + return { isAuthenticated }; +} diff --git a/client/src/index.css b/client/src/index.css index 2042bb6..bacc227 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -10,7 +10,8 @@ prefersdark: true; /* TODO: ダークモード対応 */ --color-primary: #0f82b1; --color-primary-content: #ffffff; - --color-secondary: #ff48a0; + --color-secondary: #a6e3d8; + --color-secondary-content: #065f52; } .btn { diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx new file mode 100644 index 0000000..9b44473 --- /dev/null +++ b/client/src/pages/Home.tsx @@ -0,0 +1,166 @@ +import { HiOutlineCalendar, HiOutlineCog, HiOutlinePlus, HiOutlineUser, HiOutlineUsers } from "react-icons/hi"; +import { NavLink } from "react-router"; +import { type InvolvedProjects, involvedProjectsResSchema } from "../../../common/schema"; +import Header from "../components/Header"; +import { useData } from "../hooks"; +import { API_ENDPOINT } from "../utils"; + +export default function HomePage() { + const { data: involvedProjects, loading } = useData(`${API_ENDPOINT}/projects/mine`, involvedProjectsResSchema); + + return ( + <> +
+ {loading ? ( +
+
+ +
+
+ ) : involvedProjects ? ( + + ) : ( +
+ +
+ )} + + ); +} + +function ProjectDashboard({ involvedProjects }: { involvedProjects: InvolvedProjects }) { + 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(); + }); + + return ( +
+
+ {/* Hero Section */} +
+
+ logo +

イツヒマ

+
+

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

+ + + 新しいイベントを作成 + +
+ + {involvedProjects.length > 0 ? ( +
+ {/* All Projects */} +
+

+ + あなたのイベント +

+
+ {sortedProjects.map((project) => ( + + ))} +
+
+
+ ) : ( + + )} +
+
+ ); +} + +function ProjectCard({ project }: { project: InvolvedProjects[0] }) { + return ( + +
+
+
+

{project.name}

+ + {project.isHost ? ( + <> + + 主催者 + + ) : ( + <> + + 参加者 + + )} + +
+ + {project.isHost && ( + e.stopPropagation()} + className="btn btn-ghost btn-sm px-3 py-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-all" + > + + 管理 + + )} +
+ +
+ + + {formatDate(project.startDate.toLocaleDateString())} ~{formatDate(project.endDate.toLocaleDateString())} + +
+
+ + +
+ ); +} + +function EmptyState() { + return ( +
+
+
+ +
+

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

+

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

+
+ + + イベントを作成する + +
+ ); +} + +// ---------- Utility ---------- +const formatDate = (isoDate: string) => { + const date = new Date(isoDate); + return date.toLocaleDateString("ja-JP"); +}; diff --git a/client/src/pages/Landing.tsx b/client/src/pages/Landing.tsx new file mode 100644 index 0000000..6f1ea7f --- /dev/null +++ b/client/src/pages/Landing.tsx @@ -0,0 +1,166 @@ +import { useEffect } from "react"; +import { NavLink, useNavigate } from "react-router"; +import Header from "../components/Header"; +import { useAuth } from "../hooks"; + +export default function LandingPage() { + const navigate = useNavigate(); + const { isAuthenticated } = useAuth(); + + useEffect(() => { + // If user is authenticated (has cookie), redirect to home + if (isAuthenticated === true) { + navigate("/home", { replace: true }); + } + }, [isAuthenticated, navigate]); + + if (isAuthenticated === null) { + return ( + <> +
+
+
+ +
+
+ + ); + } + + return ( + <> +
+
+ + +
+
+ + ); +} + +function HeroSection() { + return ( +
+
+
+

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

+

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

+ + 今すぐイベントを作成 + +
+
+
+ イツヒマアプリの画面 +
+
+
+ ); +} + +function FeaturesSection() { + const features = [ + { + icon: "🚫", + title: "候補日程の設定なし", + description: "みんなが空いている時間を選ぶだけなので、主催者が候補日程を設定する必要がありません。", + }, + { + icon: "🔗", + title: "URLで簡単共有", + description: "作成したイベントのURLをコピーして友達に送れば、すぐに日程調整が可能です。", + }, + { + icon: "📱", + title: "直感的な操作", + description: "複数日程も一気にドラッグで選択可能。スマホでも簡単に操作できます。", + }, + ]; + + return ( +
+
+
+

イツヒマの特長

+

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

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

{feature.title}

+

{feature.description}

+
+ ))} +
+
+
+ ); +} + +function Footer() { + return ( +
+
+
+
+

イツヒマについて

+

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

+
+ +
+
+

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

+
+
+
+ ); +} diff --git a/client/src/pages/Project.tsx b/client/src/pages/Project.tsx index ca032a2..43b552e 100644 --- a/client/src/pages/Project.tsx +++ b/client/src/pages/Project.tsx @@ -173,7 +173,9 @@ export default function ProjectPage() {
) : (
-

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

+

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

)} -
+
+ + ホームに戻る + diff --git a/client/src/pages/Root.tsx b/client/src/pages/Root.tsx deleted file mode 100644 index 8021d76..0000000 --- a/client/src/pages/Root.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { HiOutlineCog } from "react-icons/hi"; -import { NavLink } from "react-router"; -import { involvedProjectsResSchema } from "../../../common/schema"; -import Header from "../components/Header"; -import { useData } from "../hooks"; -import { API_ENDPOINT } from "../utils"; - -export default function RootPage() { - const { data: involvedProjects, loading } = useData(`${API_ENDPOINT}/projects/mine`, involvedProjectsResSchema); - - return ( - <> -
-
-
-

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

-
-
- - イベントを作成 - -
- {loading ? ( -
- -
- ) : involvedProjects ? ( -
-

作成・提出したイベント

- {involvedProjects.length > 0 ? ( -
    - {involvedProjects.map((p) => ( -
  • - -
    -

    {p.name}

    -
    - {formatDate(p.startDate.toLocaleDateString())} ~ {formatDate(p.endDate.toLocaleDateString())} -
    -
    - {p.isHost && ( -
    - - - -
    - )} -
    -
  • - ))} -
- ) : ( -

あなたが作成したイベントはありません。

- )} -
- ) : ( - - )} -
- - ); -} - -function Landing() { - return ( -
-
ランディングページ
-
- ); -} - -// ---------- Utility ---------- -const formatDate = (isoDate: string) => { - const date = new Date(isoDate); - return date.toLocaleDateString("ja-JP"); -}; diff --git a/client/src/pages/eventId/Submission.tsx b/client/src/pages/eventId/Submission.tsx index 721968a..9cfa006 100644 --- a/client/src/pages/eventId/Submission.tsx +++ b/client/src/pages/eventId/Submission.tsx @@ -1,5 +1,11 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { HiOutlineCheckCircle, HiOutlineCog, HiOutlineExclamationCircle, HiPencil } from "react-icons/hi"; +import { + HiOutlineCheckCircle, + HiOutlineCog, + HiOutlineExclamationCircle, + HiOutlineHome, + HiPencil, +} from "react-icons/hi"; import { NavLink, useParams } from "react-router"; import { projectResSchema } from "../../../../common/schema"; import { Calendar } from "../../components/Calendar"; @@ -121,7 +127,7 @@ export default function SubmissionPage() { ) : (
-

{project.name} の日程調整

+

{project.name} の日程調整

{isHost && ( @@ -174,9 +180,12 @@ export default function SubmissionPage() { ) : ( <> - + + + ホームに戻る +