Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,58 @@
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
import { BrowserRouter, Navigate, Outlet, Route, Routes, useParams } from "react-router";
import SubmissionPage from "./pages/eventId/Submission.tsx";
import HomePage from "./pages/Home.tsx";
import LandingPage from "./pages/Landing.tsx";
import NotFoundPage from "./pages/NotFound.tsx";
import ProjectPage from "./pages/Project.tsx";

/**
* Nano ID 形式の正規表現。
* 現在はハイフン・アンダースコアを含まないが、初期は含んでいたため URL の検証時は両方許容する。
*/
const NANOID_REGEX = /^[A-Za-z0-9_-]{21}$/;

/**
* 旧パス /:eventId から新パス /e/:eventId へのリダイレクト。eventId は Nano ID 形式。
*/
function LegacyEventRedirect() {
const { eventId } = useParams();
if (!eventId || !NANOID_REGEX.test(eventId)) {
return <NotFoundPage />;
}
return <Navigate to={`/e/${eventId}`} replace />;
}

/**
* 旧パス /:eventId/edit から新パス /e/:eventId/edit へのリダイレクト。eventId は Nano ID 形式。
*/
function LegacyEventEditRedirect() {
const { eventId } = useParams();
if (!eventId || !NANOID_REGEX.test(eventId)) {
return <NotFoundPage />;
}
return <Navigate to={`/e/${eventId}/edit`} replace />;
}

export default function App() {
return (
<BrowserRouter>
<Routes>
<Route index element={<LandingPage />} />
<Route path="home" element={<HomePage />} />
<Route path="new" element={<ProjectPage />} />
<Route path=":eventId" element={<Outlet />}>
<Route index element={<SubmissionPage />} />
<Route path="edit" element={<ProjectPage />} />
<Route path="*" element={<NotFoundPage />} />

<Route path="e">
<Route path=":eventId" element={<Outlet />}>
<Route index element={<SubmissionPage />} />
<Route path="edit" element={<ProjectPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Route>

{/* /:eventId (旧形式) を /e/:eventId (新形式) にリダイレクト */}
<Route path=":eventId" element={<LegacyEventRedirect />} />
<Route path=":eventId/edit" element={<LegacyEventEditRedirect />} />

<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function ProjectDashboard({ involvedProjects }: { involvedProjects: BriefProject
function ProjectCard({ project }: { project: BriefProject }) {
return (
<NavLink
to={`/${project.id}`}
to={`/e/${project.id}`}
className={`group hover:-translate-y-1 relative block transform overflow-hidden rounded-xl border-l-4 bg-white shadow-lg transition-all duration-300 hover:shadow-xl ${project.isHost ? "border-primary" : "border-secondary"} focus:outline-none focus:ring-4 focus:ring-primary/20`}
aria-label={`「${project.name}」の詳細を見る`}
>
Expand Down
10 changes: 5 additions & 5 deletions client/src/pages/Project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export default function ProjectPage() {
useEffect(() => {
if (!loading && project && !isHost) {
if (eventId) {
navigate(`/${eventId}`);
navigate(`/e/${eventId}`);
} else {
navigate("/");
}
Expand Down Expand Up @@ -613,7 +613,7 @@ export default function ProjectPage() {
)}
<div className="fixed bottom-0 left-0 flex max-h-18 w-full justify-between bg-white p-4 shadow-[0_-2px_8px_rgba(0,0,0,0.1)]">
{eventId ? (
<NavLink to={`/${eventId}`} className="btn btn-outline btn-primary">
<NavLink to={`/e/${eventId}`} className="btn btn-outline btn-primary">
日程調整に戻る
</NavLink>
) : (
Expand All @@ -639,12 +639,12 @@ export default function ProjectPage() {
type="text"
disabled
className="input input-info w-full"
value={`${FRONTEND_ORIGIN}/${dialogStatus.projectId}`}
value={`${FRONTEND_ORIGIN}/e/${dialogStatus.projectId}`}
/>
<button
type="button"
onClick={async () => {
await navigator.clipboard.writeText(`${FRONTEND_ORIGIN}/${dialogStatus.projectId}`);
await navigator.clipboard.writeText(`${FRONTEND_ORIGIN}/e/${dialogStatus.projectId}`);
setCopied(true);
setTimeout(() => {
setCopied(false);
Expand All @@ -657,7 +657,7 @@ export default function ProjectPage() {
</button>
</div>
<div className="modal-action">
<NavLink className="btn btn-primary" to={`/${dialogStatus.projectId}`}>
<NavLink className="btn btn-primary" to={`/e/${dialogStatus.projectId}`}>
イベントへ
</NavLink>
</div>
Expand Down
8 changes: 6 additions & 2 deletions client/src/pages/eventId/Submission.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export default function SubmissionPage() {
<>
<div className="flex h-[100dvh] flex-col">
<Header />
{loading || !selectedParticipationOptionId ? (
{loading ? (
<div className="flex w-full flex-1 items-center justify-center">
<span className="loading loading-dots loading-md text-gray-400" />
</div>
Expand All @@ -221,12 +221,16 @@ export default function SubmissionPage() {
ホームに戻る
</NavLink>
</div>
) : !selectedParticipationOptionId ? (
<div className="flex w-full flex-1 items-center justify-center">
<span className="loading loading-dots loading-md text-gray-400" />
</div>
) : (
<div className="flex h-full flex-1 flex-col overflow-y-auto p-4">
<div className="flex items-center justify-between">
<h1 className="mb-2 font-bold text-2xl text-gray-800">{project.name} の日程調整</h1>
{isHost && (
<NavLink to={`/${projectId}/edit`} className="btn btn-sm font-normal text-gray-600">
<NavLink to={`/e/${projectId}/edit`} className="btn btn-sm font-normal text-gray-600">
<HiOutlineCog />
イベント設定
</NavLink>
Expand Down
7 changes: 6 additions & 1 deletion server/src/routes/projects.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { zValidator } from "@hono/zod-validator";
import dotenv from "dotenv";
import { Hono } from "hono";
import { nanoid } from "nanoid";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { editReqSchema, projectReqSchema, submitReqSchema } from "../../../common/validators.js";
import { prisma } from "../main.js";

dotenv.config();

/**
* ハイフン・アンダースコアを含まない Nano ID 形式。
*/
const nanoid = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 21);

const projectIdParamsSchema = z.object({ projectId: z.string().length(21) });

type AppVariables = {
Expand Down