Skip to content

Commit 1050dba

Browse files
authored
Merge pull request #41 from ut-code/develop
release: ランディングページ・ダッシュボードを中心に UI を再構成
2 parents ce1e03a + e004eb7 commit 1050dba

File tree

14 files changed

+404
-94
lines changed

14 files changed

+404
-94
lines changed

client/index.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
<head>
55
<meta charset="UTF-8" />
6-
<title>イツヒマ</title>
6+
<title>イツヒマ - 「いつヒマ?」で日程調整しよう</title>
77
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
88
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
99
<meta property="og:type" content="website" />
10-
<meta property="og:title" content="イツヒマ" />
11-
<meta property="og:description" content="「いつ暇?」で日程調整しよう" />
12-
<meta property="og:image" content="https://itsuhima.utcode.net/og-image.webp" />
10+
<meta property="og:title" content="イツヒマ - 「いつヒマ?」で日程調整しよう" />
11+
<meta property="og:description" content="とりあえずみんなの空いている時間を訊いてから、何を何時間やるか決めたい。そんな仲間うちでの日程調整に最適なツールです。" />
12+
<meta property="og:image" content="https://itsuhima.utcode.net/og-image.jpg" />
1313
<meta property="og:site_name" content="イツヒマ" />
1414
<meta property="og:locale" content="ja_JP" />
1515
<meta name="twitter:card" content="summary_large_image" />

client/public/mock-mobile.png

690 KB
Loading

client/public/og-image.jpg

22.2 KB
Loading

client/public/og-image.webp

-19.6 KB
Binary file not shown.

client/src/App.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
2+
import HomePage from "./pages/Home.tsx";
3+
import LandingPage from "./pages/Landing.tsx";
24
import NotFoundPage from "./pages/NotFound.tsx";
35
import ProjectPage from "./pages/Project.tsx";
4-
import RootPage from "./pages/Root.tsx";
56
import SubmissionPage from "./pages/eventId/Submission.tsx";
67

78
export default function App() {
89
return (
910
<BrowserRouter>
1011
<Routes>
11-
<Route index element={<RootPage />} />
12+
<Route index element={<LandingPage />} />
13+
<Route path="home" element={<HomePage />} />
1214
<Route path="new" element={<ProjectPage />} />
1315
<Route path=":eventId" element={<Outlet />}>
1416
<Route index element={<SubmissionPage />} />

client/src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default function Header() {
55
return (
66
<div className="navbar bg-primary shadow-sm sticky top-0 left-0 z-50">
77
<div className="flex-1">
8-
<NavLink className="flex text-2xl text-white items-center px-2 gap-1 font-mplus" to="/">
8+
<NavLink className="flex text-2xl text-white items-center px-2 gap-1 font-mplus" to="/home">
99
<img src="/logo-white.svg" alt="logo" width={24} />
1010
<span className="px-2">イツヒマ</span>
1111
<span className="text-xs">(アルファ版)</span>

client/src/hooks.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback, useEffect, useState } from "react";
22
import type { ZodType, ZodTypeDef } from "zod";
3+
import { API_ENDPOINT } from "./utils";
34

45
export function useData<O, I>(
56
url: string | null,
@@ -48,3 +49,42 @@ export function useData<O, I>(
4849

4950
return { data, loading, error, refetch: fetchData };
5051
}
52+
53+
export function useAuth(): { isAuthenticated: boolean | null } {
54+
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
55+
56+
useEffect(() => {
57+
const controller = new AbortController();
58+
59+
const checkAuth = async () => {
60+
try {
61+
const res = await fetch(`${API_ENDPOINT}/projects/mine`, {
62+
method: "GET",
63+
credentials: "include",
64+
signal: controller.signal,
65+
});
66+
67+
if (res.ok) {
68+
setIsAuthenticated(true);
69+
} else if (res.status === 401 || res.status === 403) {
70+
setIsAuthenticated(false);
71+
} else {
72+
setIsAuthenticated(false);
73+
console.error(`Unexpected response: ${res.status} ${res.statusText}`);
74+
}
75+
} catch (err) {
76+
if (err instanceof DOMException && err.name === "AbortError") {
77+
return;
78+
}
79+
setIsAuthenticated(false);
80+
console.error("Auth check failed:", err);
81+
}
82+
};
83+
84+
checkAuth();
85+
86+
return () => controller.abort();
87+
}, []);
88+
89+
return { isAuthenticated };
90+
}

client/src/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
prefersdark: true; /* TODO: ダークモード対応 */
1111
--color-primary: #0f82b1;
1212
--color-primary-content: #ffffff;
13-
--color-secondary: #ff48a0;
13+
--color-secondary: #a6e3d8;
14+
--color-secondary-content: #065f52;
1415
}
1516

1617
.btn {

client/src/pages/Home.tsx

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { HiOutlineCalendar, HiOutlineCog, HiOutlinePlus, HiOutlineUser, HiOutlineUsers } from "react-icons/hi";
2+
import { NavLink } from "react-router";
3+
import { type InvolvedProjects, involvedProjectsResSchema } from "../../../common/schema";
4+
import Header from "../components/Header";
5+
import { useData } from "../hooks";
6+
import { API_ENDPOINT } from "../utils";
7+
8+
export default function HomePage() {
9+
const { data: involvedProjects, loading } = useData(`${API_ENDPOINT}/projects/mine`, involvedProjectsResSchema);
10+
11+
return (
12+
<>
13+
<Header />
14+
{loading ? (
15+
<div className="min-h-[calc(100dvh_-_64px)] bg-blue-50 flex items-center justify-center">
16+
<div className="py-4">
17+
<span className="loading loading-dots loading-md text-gray-400" />
18+
</div>
19+
</div>
20+
) : involvedProjects ? (
21+
<ProjectDashboard involvedProjects={involvedProjects} />
22+
) : (
23+
<div className="min-h-[calc(100dvh_-_64px)] bg-blue-50 flex items-center justify-center">
24+
<EmptyState />
25+
</div>
26+
)}
27+
</>
28+
);
29+
}
30+
31+
function ProjectDashboard({ involvedProjects }: { involvedProjects: InvolvedProjects }) {
32+
const sortedProjects = [...involvedProjects].sort((a, b) => {
33+
if (a.isHost !== b.isHost) {
34+
return a.isHost ? -1 : 1;
35+
}
36+
return new Date(b.startDate).getTime() - new Date(a.startDate).getTime();
37+
});
38+
39+
return (
40+
<div className="min-h-[calc(100dvh_-_64px)] bg-blue-50">
41+
<div className="container mx-auto px-4 py-8">
42+
{/* Hero Section */}
43+
<div className="text-center mb-12">
44+
<div className="flex items-center justify-center mt-2 mb-6">
45+
<img src="/logo.svg" alt="logo" width={48} className="mr-4" />
46+
<h1 className="text-4xl text-primary font-mplus">イツヒマ</h1>
47+
</div>
48+
<p className="text-xl text-gray-600 mb-8">「いつヒマ?」で日程調整しよう</p>
49+
<NavLink
50+
to="/new"
51+
className="btn btn-primary btn-lg px-8 py-4 text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1"
52+
>
53+
<HiOutlinePlus className="mr-2" size={20} />
54+
新しいイベントを作成
55+
</NavLink>
56+
</div>
57+
58+
{involvedProjects.length > 0 ? (
59+
<div className="space-y-8">
60+
{/* All Projects */}
61+
<section>
62+
<h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center">
63+
<HiOutlineCalendar className="mr-3 text-gray-700" size={28} />
64+
あなたのイベント
65+
</h2>
66+
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
67+
{sortedProjects.map((project) => (
68+
<ProjectCard key={project.id} project={project} />
69+
))}
70+
</div>
71+
</section>
72+
</div>
73+
) : (
74+
<EmptyState />
75+
)}
76+
</div>
77+
</div>
78+
);
79+
}
80+
81+
function ProjectCard({ project }: { project: InvolvedProjects[0] }) {
82+
return (
83+
<NavLink
84+
to={`/${project.id}`}
85+
className={`group relative block bg-white rounded-xl shadow-lg hover:shadow-xl
86+
transition-all duration-300 transform hover:-translate-y-1 overflow-hidden
87+
border-l-4 ${project.isHost ? "border-primary" : "border-secondary"}
88+
focus:outline-none focus:ring-4 focus:ring-primary/20`}
89+
aria-label={`「${project.name}」の詳細を見る`}
90+
>
91+
<div className="p-6">
92+
<div className="flex justify-between items-start mb-4">
93+
<div className="flex-1 mr-2">
94+
<h3 className="text-xl font-semibold text-gray-800 mb-2 break-words">{project.name}</h3>
95+
<span className={`badge badge-sm ${project.isHost ? "badge-primary" : "badge-secondary"}`}>
96+
{project.isHost ? (
97+
<>
98+
<HiOutlineUser size={12} />
99+
<span>主催者</span>
100+
</>
101+
) : (
102+
<>
103+
<HiOutlineUsers size={12} />
104+
<span>参加者</span>
105+
</>
106+
)}
107+
</span>
108+
</div>
109+
110+
{project.isHost && (
111+
<NavLink
112+
to={`/${project.id}/edit`}
113+
onClick={(e) => e.stopPropagation()}
114+
className="btn btn-ghost btn-sm px-3 py-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-all"
115+
>
116+
<HiOutlineCog size={14} />
117+
<span className="text-xs">管理</span>
118+
</NavLink>
119+
)}
120+
</div>
121+
122+
<div className="flex items-center text-gray-600 mb-4">
123+
<HiOutlineCalendar className="mr-2" size={16} />
124+
<span className="text-sm">
125+
{formatDate(project.startDate.toLocaleDateString())}{formatDate(project.endDate.toLocaleDateString())}
126+
</span>
127+
</div>
128+
</div>
129+
130+
<span
131+
className={`absolute bottom-4 right-4 ${project.isHost ? "text-primary" : "text-secondary"} text-2xl pointer-events-none
132+
group-hover:translate-x-1 transition-transform`}
133+
aria-hidden="true"
134+
>
135+
&rsaquo;
136+
</span>
137+
</NavLink>
138+
);
139+
}
140+
141+
function EmptyState() {
142+
return (
143+
<div className="text-center py-16">
144+
<div className="mb-8">
145+
<div className="w-32 h-32 mx-auto mb-6 bg-gray-100 rounded-full flex items-center justify-center">
146+
<HiOutlineCalendar className="text-gray-400" size={64} />
147+
</div>
148+
<h3 className="text-2xl font-semibold text-gray-800 mb-3">まだイベントがありません</h3>
149+
<p className="text-gray-600 mb-8 max-w-md mx-auto">イベントを作成して、日程調整を始めましょう</p>
150+
</div>
151+
<NavLink
152+
to="/new"
153+
className="btn btn-primary btn-lg px-8 py-4 text-lg shadow-lg hover:shadow-xl transition-all duration-300"
154+
>
155+
<HiOutlinePlus className="mr-2" size={20} />
156+
イベントを作成する
157+
</NavLink>
158+
</div>
159+
);
160+
}
161+
162+
// ---------- Utility ----------
163+
const formatDate = (isoDate: string) => {
164+
const date = new Date(isoDate);
165+
return date.toLocaleDateString("ja-JP");
166+
};

0 commit comments

Comments
 (0)