refactor: standardize code formatting across components and utilities
Some checks failed
Push to github container register / push-docker (push) Has been cancelled
Some checks failed
Push to github container register / push-docker (push) Has been cancelled
- Updated formatting in SiteFooter and SiteHeader components for consistency. - Refactored Badge, Button, Card, Separator components to improve readability. - Enhanced markdown parsing logic in announcements and markdown utility functions. - Adjusted ESLint configuration for better code quality checks. - Added biome.json for BiomeJS configuration. - Updated package.json and configuration files for improved dependency management.
This commit is contained in:
@@ -3,64 +3,75 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { getAnnouncementBySlug, getAnnouncementSlugs } from "@/lib/announcements";
|
||||
import {
|
||||
getAnnouncementBySlug,
|
||||
getAnnouncementSlugs,
|
||||
} from "@/lib/announcements";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ slug: string }>;
|
||||
params: Promise<{ slug: string }>;
|
||||
};
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const slugs = await getAnnouncementSlugs();
|
||||
return slugs.map((slug) => ({ slug }));
|
||||
const slugs = await getAnnouncementSlugs();
|
||||
return slugs.map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
const announcement = await getAnnouncementBySlug(slug);
|
||||
return {
|
||||
title: `${announcement.title} | お知らせ`,
|
||||
description: announcement.summary || `${announcement.title} のお知らせです。`,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
title: "お知らせ",
|
||||
description: "お知らせページ",
|
||||
};
|
||||
}
|
||||
try {
|
||||
const announcement = await getAnnouncementBySlug(slug);
|
||||
return {
|
||||
title: `${announcement.title} | お知らせ`,
|
||||
description:
|
||||
announcement.summary || `${announcement.title} のお知らせです。`,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
title: "お知らせ",
|
||||
description: "お知らせページ",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function AnnouncementDetailPage({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
const { slug } = await params;
|
||||
|
||||
let announcement;
|
||||
try {
|
||||
announcement = await getAnnouncementBySlug(slug);
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
let announcement;
|
||||
try {
|
||||
announcement = await getAnnouncementBySlug(slug);
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="mx-auto flex w-full max-w-4xl flex-1 flex-col gap-6 px-4 py-8 sm:px-8 sm:py-12">
|
||||
<header className="rounded-3xl border bg-card/70 p-6 shadow-sm backdrop-blur motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:duration-700 sm:p-8">
|
||||
<Badge variant="outline" className="mb-3">
|
||||
お知らせ詳細
|
||||
</Badge>
|
||||
<p className="text-xs text-muted-foreground">{announcement.date}</p>
|
||||
<h1 className="mt-2 text-2xl font-semibold tracking-tight sm:text-4xl">{announcement.title}</h1>
|
||||
</header>
|
||||
return (
|
||||
<article className="mx-auto flex w-full max-w-4xl flex-1 flex-col gap-6 px-4 py-8 sm:px-8 sm:py-12">
|
||||
<header className="rounded-3xl border bg-card/70 p-6 shadow-sm backdrop-blur motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:duration-700 sm:p-8">
|
||||
<Badge variant="outline" className="mb-3">
|
||||
お知らせ詳細
|
||||
</Badge>
|
||||
<p className="text-xs text-muted-foreground">{announcement.date}</p>
|
||||
<h1 className="mt-2 text-2xl font-semibold tracking-tight sm:text-4xl">
|
||||
{announcement.title}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<section className="rounded-3xl border bg-card p-6 shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-2 motion-safe:duration-700 sm:p-8">
|
||||
<div
|
||||
className="markdown-body space-y-4 text-sm leading-7 sm:text-base [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-2 [&_blockquote]:pl-3 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_h1]:text-2xl [&_h1]:font-semibold [&_h2]:text-xl [&_h2]:font-semibold [&_h3]:text-lg [&_h3]:font-semibold [&_li]:ml-5 [&_ol]:list-decimal [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-3 [&_ul]:list-disc"
|
||||
dangerouslySetInnerHTML={{ __html: announcement.contentHtml }}
|
||||
/>
|
||||
</section>
|
||||
<section className="rounded-3xl border bg-card p-6 shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-2 motion-safe:duration-700 sm:p-8">
|
||||
<div
|
||||
className="markdown-body space-y-4 text-sm leading-7 sm:text-base [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-2 [&_blockquote]:pl-3 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_h1]:text-2xl [&_h1]:font-semibold [&_h2]:text-xl [&_h2]:font-semibold [&_h3]:text-lg [&_h3]:font-semibold [&_li]:ml-5 [&_ol]:list-decimal [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-3 [&_ul]:list-disc"
|
||||
dangerouslySetInnerHTML={{ __html: announcement.contentHtml }}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Link href="/announcements" className="text-sm text-primary underline underline-offset-4 transition-transform duration-200 hover:-translate-y-0.5">
|
||||
お知らせ一覧へ戻る
|
||||
</Link>
|
||||
</article>
|
||||
);
|
||||
<Link
|
||||
href="/announcements"
|
||||
className="text-sm text-primary underline underline-offset-4 transition-transform duration-200 hover:-translate-y-0.5"
|
||||
>
|
||||
お知らせ一覧へ戻る
|
||||
</Link>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export default function AnnouncementsLoading() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-1 items-center justify-center px-4 py-12 sm:px-8">
|
||||
<div className="flex items-center gap-3 rounded-xl border bg-card px-5 py-3 text-sm text-muted-foreground shadow-sm">
|
||||
<span className="inline-block size-2.5 animate-pulse rounded-full bg-primary" />
|
||||
お知らせを読み込み中...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-1 items-center justify-center px-4 py-12 sm:px-8">
|
||||
<div className="flex items-center gap-3 rounded-xl border bg-card px-5 py-3 text-sm text-muted-foreground shadow-sm">
|
||||
<span className="inline-block size-2.5 animate-pulse rounded-full bg-primary" />
|
||||
お知らせを読み込み中...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,43 +4,48 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { getAllAnnouncements } from "@/lib/announcements";
|
||||
|
||||
export const metadata = {
|
||||
title: "お知らせ | Takasumi-Neodyマイクラサーバプロジェクト",
|
||||
description: "Takasumi-Neodyマイクラサーバプロジェクトのお知らせ一覧です。",
|
||||
title: "お知らせ | Takasumi-Neodyマイクラサーバプロジェクト",
|
||||
description: "Takasumi-Neodyマイクラサーバプロジェクトのお知らせ一覧です。",
|
||||
};
|
||||
|
||||
export default async function AnnouncementsPage() {
|
||||
const announcements = await getAllAnnouncements();
|
||||
const announcements = await getAllAnnouncements();
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-8 sm:px-8 sm:py-12">
|
||||
<header className="rounded-3xl border bg-card/70 p-6 shadow-sm backdrop-blur motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:duration-700 sm:p-8">
|
||||
<Badge variant="outline" className="mb-3">
|
||||
お知らせ
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">更新情報・告知</h1>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground sm:text-base">
|
||||
このページは Markdown で作成したお知らせを表示します。
|
||||
</p>
|
||||
</header>
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-8 sm:px-8 sm:py-12">
|
||||
<header className="rounded-3xl border bg-card/70 p-6 shadow-sm backdrop-blur motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:duration-700 sm:p-8">
|
||||
<Badge variant="outline" className="mb-3">
|
||||
お知らせ
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">
|
||||
更新情報・告知
|
||||
</h1>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground sm:text-base">
|
||||
このページは Markdown で作成したお知らせを表示します。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="grid gap-4">
|
||||
{announcements.map((item) => (
|
||||
<article className="rounded-2xl border border-foreground/10 bg-card p-5 shadow-xs transition-all duration-300 motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-4 hover:-translate-y-1 hover:shadow-md sm:p-6" key={item.slug}>
|
||||
<header>
|
||||
<p className="text-xs text-muted-foreground">{item.date}</p>
|
||||
<h2 className="mt-1 text-xl font-semibold">
|
||||
<Link
|
||||
href={`/announcements/${item.slug}`}
|
||||
className="underline-offset-4 transition-colors duration-200 hover:text-primary hover:underline"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</h2>
|
||||
</header>
|
||||
<p className="mt-3 text-sm text-muted-foreground">{item.summary}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
<section className="grid gap-4">
|
||||
{announcements.map((item) => (
|
||||
<article
|
||||
className="rounded-2xl border border-foreground/10 bg-card p-5 shadow-xs transition-all duration-300 motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-4 hover:-translate-y-1 hover:shadow-md sm:p-6"
|
||||
key={item.slug}
|
||||
>
|
||||
<header>
|
||||
<p className="text-xs text-muted-foreground">{item.date}</p>
|
||||
<h2 className="mt-1 text-xl font-semibold">
|
||||
<Link
|
||||
href={`/announcements/${item.slug}`}
|
||||
className="underline-offset-4 transition-colors duration-200 hover:text-primary hover:underline"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</h2>
|
||||
</header>
|
||||
<p className="mt-3 text-sm text-muted-foreground">{item.summary}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
226
app/globals.css
226
app/globals.css
@@ -5,126 +5,126 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-heading: var(--font-sans);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-heading: var(--font-sans);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
@@ -5,38 +5,46 @@ import { cn } from "@/lib/utils";
|
||||
import { SiteHeader } from "@/components/site-header";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
|
||||
const inter = Inter({subsets:['latin'],variable:'--font-sans'});
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Takasumi-Neodyマイクラサーバプロジェクト接続ガイド",
|
||||
description: "Takasumi-Neodyマイクラサーバプロジェクト(サバイバル鯖・建築鯖)への接続方法とサーバアドレスを案内します。",
|
||||
title: "Takasumi-Neodyマイクラサーバプロジェクト接続ガイド",
|
||||
description:
|
||||
"Takasumi-Neodyマイクラサーバプロジェクト(サバイバル鯖・建築鯖)への接続方法とサーバアドレスを案内します。",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="ja"
|
||||
className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, "font-sans", inter.variable)}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">
|
||||
<SiteHeader />
|
||||
{children}
|
||||
<SiteFooter />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
return (
|
||||
<html
|
||||
lang="ja"
|
||||
className={cn(
|
||||
"h-full",
|
||||
"antialiased",
|
||||
geistSans.variable,
|
||||
geistMono.variable,
|
||||
"font-sans",
|
||||
inter.variable,
|
||||
)}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">
|
||||
<SiteHeader />
|
||||
{children}
|
||||
<SiteFooter />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-1 items-center justify-center px-4 py-16 sm:px-8">
|
||||
<div className="flex items-center gap-3 rounded-xl border bg-card px-5 py-3 text-sm text-muted-foreground shadow-sm">
|
||||
<span className="inline-block size-2.5 animate-pulse rounded-full bg-primary" />
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-1 items-center justify-center px-4 py-16 sm:px-8">
|
||||
<div className="flex items-center gap-3 rounded-xl border bg-card px-5 py-3 text-sm text-muted-foreground shadow-sm">
|
||||
<span className="inline-block size-2.5 animate-pulse rounded-full bg-primary" />
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
202
app/page.tsx
202
app/page.tsx
@@ -9,104 +9,116 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const servers = [
|
||||
{
|
||||
name: "サバイバル鯖",
|
||||
description: "採掘・冒険・建築を楽しむ通常ワールドです。",
|
||||
address: "survival.mc.neody.ad.jp",
|
||||
icon: Sprout,
|
||||
badgeVariant: "default" as const,
|
||||
},
|
||||
{
|
||||
name: "建築鯖",
|
||||
description: "大型建築や街づくり向けのクリエイティブ環境です。",
|
||||
address: "kenchiku.mc.neody.ad.jp",
|
||||
icon: Pickaxe,
|
||||
badgeVariant: "secondary" as const,
|
||||
},
|
||||
{
|
||||
name: "サバイバル鯖",
|
||||
description: "採掘・冒険・建築を楽しむ通常ワールドです。",
|
||||
address: "survival.mc.neody.ad.jp",
|
||||
icon: Sprout,
|
||||
badgeVariant: "default" as const,
|
||||
},
|
||||
{
|
||||
name: "建築鯖",
|
||||
description: "大型建築や街づくり向けのクリエイティブ環境です。",
|
||||
address: "kenchiku.mc.neody.ad.jp",
|
||||
icon: Pickaxe,
|
||||
badgeVariant: "secondary" as const,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="relative flex min-h-full flex-1 flex-col overflow-x-clip bg-background">
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[380px] bg-gradient-to-b from-emerald-200/40 via-sky-200/25 to-transparent blur-3xl dark:from-emerald-500/20 dark:via-sky-500/20" />
|
||||
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-8 px-4 py-8 sm:gap-10 sm:px-8 sm:py-14">
|
||||
<section className="rounded-3xl border bg-card/70 p-6 shadow-sm backdrop-blur motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:duration-700 sm:p-10">
|
||||
<Badge variant="outline" className="mb-4 max-w-full text-[11px] sm:text-xs">
|
||||
Takasumi-Neodyマイクラサーバプロジェクト接続ガイド
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-semibold tracking-tight sm:text-5xl">
|
||||
Minecraft サーバへ接続する
|
||||
</h1>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-muted-foreground sm:text-base">
|
||||
サーバー追加画面で下記アドレスを入力してください。Java版 と
|
||||
統合版のどちらにも対応しています。
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/announcements"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"transition-transform duration-200 hover:-translate-y-0.5"
|
||||
)}
|
||||
>
|
||||
お知らせを見る
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
return (
|
||||
<div className="relative flex min-h-full flex-1 flex-col overflow-x-clip bg-background">
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[380px] bg-gradient-to-b from-emerald-200/40 via-sky-200/25 to-transparent blur-3xl dark:from-emerald-500/20 dark:via-sky-500/20" />
|
||||
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-8 px-4 py-8 sm:gap-10 sm:px-8 sm:py-14">
|
||||
<section className="rounded-3xl border bg-card/70 p-6 shadow-sm backdrop-blur motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:duration-700 sm:p-10">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="mb-4 max-w-full text-[11px] sm:text-xs"
|
||||
>
|
||||
Takasumi-Neodyマイクラサーバプロジェクト接続ガイド
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-semibold tracking-tight sm:text-5xl">
|
||||
Minecraft サーバへ接続する
|
||||
</h1>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-muted-foreground sm:text-base">
|
||||
サーバー追加画面で下記アドレスを入力してください。Java版 と
|
||||
統合版のどちらにも対応しています。
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/announcements"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"transition-transform duration-200 hover:-translate-y-0.5",
|
||||
)}
|
||||
>
|
||||
お知らせを見る
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2">
|
||||
{servers.map((server) => {
|
||||
const Icon = server.icon;
|
||||
return (
|
||||
<article
|
||||
key={server.name}
|
||||
className="rounded-2xl border border-foreground/10 bg-card p-5 shadow-xs transition-all duration-300 motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-4 hover:-translate-y-1 hover:shadow-md sm:p-6"
|
||||
>
|
||||
<header>
|
||||
<div className="mb-2 flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<Badge variant={server.badgeVariant}>{server.name}</Badge>
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Wifi className="size-3.5" />
|
||||
Java版 / 統合版 対応
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="flex items-center gap-2 text-xl font-semibold">
|
||||
<Icon className="size-5" />
|
||||
{server.name}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{server.description}</p>
|
||||
</header>
|
||||
<div className="mt-5">
|
||||
<div className="rounded-xl border bg-background px-4 py-3">
|
||||
<p className="text-xs text-muted-foreground">サーバアドレス</p>
|
||||
<p className="mt-1 break-all font-mono text-sm">{server.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex gap-2">
|
||||
<a
|
||||
className={cn(buttonVariants(), "w-full sm:w-auto")}
|
||||
href={`minecraft://?addExternalServer=${server.name}|${server.address}`}
|
||||
>
|
||||
Minecraft で開く
|
||||
<ExternalLink className="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<section className="grid gap-4 md:grid-cols-2">
|
||||
{servers.map((server) => {
|
||||
const Icon = server.icon;
|
||||
return (
|
||||
<article
|
||||
key={server.name}
|
||||
className="rounded-2xl border border-foreground/10 bg-card p-5 shadow-xs transition-all duration-300 motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-4 hover:-translate-y-1 hover:shadow-md sm:p-6"
|
||||
>
|
||||
<header>
|
||||
<div className="mb-2 flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<Badge variant={server.badgeVariant}>{server.name}</Badge>
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Wifi className="size-3.5" />
|
||||
Java版 / 統合版 対応
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="flex items-center gap-2 text-xl font-semibold">
|
||||
<Icon className="size-5" />
|
||||
{server.name}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{server.description}
|
||||
</p>
|
||||
</header>
|
||||
<div className="mt-5">
|
||||
<div className="rounded-xl border bg-background px-4 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
サーバアドレス
|
||||
</p>
|
||||
<p className="mt-1 break-all font-mono text-sm">
|
||||
{server.address}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex gap-2">
|
||||
<a
|
||||
className={cn(buttonVariants(), "w-full sm:w-auto")}
|
||||
href={`minecraft://?addExternalServer=${server.name}|${server.address}`}
|
||||
>
|
||||
Minecraft で開く
|
||||
<ExternalLink className="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="rounded-3xl border bg-card p-6 shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-2 motion-safe:duration-700 sm:p-8">
|
||||
<h2 className="text-xl font-semibold">接続手順</h2>
|
||||
<Separator className="my-4" />
|
||||
<ol className="space-y-3 text-sm leading-7 sm:text-base">
|
||||
<li>1. Minecraft(Java版 または統合版)を起動する。</li>
|
||||
<li>2. サーバー追加画面を開く(Java版: マルチプレイ / 統合版: サーバー)。</li>
|
||||
<li>3. 接続したい鯖のサーバアドレスを入力して保存する。</li>
|
||||
<li>4. 一覧からサーバを選んで接続する。</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
<section className="rounded-3xl border bg-card p-6 shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-2 motion-safe:duration-700 sm:p-8">
|
||||
<h2 className="text-xl font-semibold">接続手順</h2>
|
||||
<Separator className="my-4" />
|
||||
<ol className="space-y-3 text-sm leading-7 sm:text-base">
|
||||
<li>1. Minecraft(Java版 または統合版)を起動する。</li>
|
||||
<li>
|
||||
2. サーバー追加画面を開く(Java版: マルチプレイ / 統合版:
|
||||
サーバー)。
|
||||
</li>
|
||||
<li>3. 接続したい鯖のサーバアドレスを入力して保存する。</li>
|
||||
<li>4. 一覧からサーバを選んで接続する。</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,42 +5,46 @@ import { getAllAnnouncements } from "@/lib/announcements";
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const SITE_URL =
|
||||
process.env.NEXT_PUBLIC_SITE_URL ??
|
||||
process.env.SITE_URL ??
|
||||
"https://mc.neody.ad.jp";
|
||||
process.env.NEXT_PUBLIC_SITE_URL ??
|
||||
process.env.SITE_URL ??
|
||||
"https://mc.neody.ad.jp";
|
||||
|
||||
function toAbsoluteUrl(pathname: string): string {
|
||||
return new URL(pathname, SITE_URL).toString();
|
||||
return new URL(pathname, SITE_URL).toString();
|
||||
}
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const announcements = await getAllAnnouncements();
|
||||
const announcements = await getAllAnnouncements();
|
||||
|
||||
const staticRoutes: MetadataRoute.Sitemap = [
|
||||
{
|
||||
url: toAbsoluteUrl("/"),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: toAbsoluteUrl("/announcements/"),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
];
|
||||
const staticRoutes: MetadataRoute.Sitemap = [
|
||||
{
|
||||
url: toAbsoluteUrl("/"),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: toAbsoluteUrl("/announcements/"),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
];
|
||||
|
||||
const announcementRoutes: MetadataRoute.Sitemap = announcements.map((item) => {
|
||||
const parsedDate = new Date(item.date);
|
||||
const announcementRoutes: MetadataRoute.Sitemap = announcements.map(
|
||||
(item) => {
|
||||
const parsedDate = new Date(item.date);
|
||||
|
||||
return {
|
||||
url: toAbsoluteUrl(`/announcements/${item.slug}/`),
|
||||
lastModified: Number.isNaN(parsedDate.getTime()) ? new Date() : parsedDate,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
};
|
||||
});
|
||||
return {
|
||||
url: toAbsoluteUrl(`/announcements/${item.slug}/`),
|
||||
lastModified: Number.isNaN(parsedDate.getTime())
|
||||
? new Date()
|
||||
: parsedDate,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return [...staticRoutes, ...announcementRoutes];
|
||||
return [...staticRoutes, ...announcementRoutes];
|
||||
}
|
||||
|
||||
8
biome.json
Normal file
8
biome.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "base-vega",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "base-vega",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="border-t bg-card/60">
|
||||
<div className="mx-auto flex w-full max-w-6xl items-center justify-center px-4 py-4 text-center text-xs text-muted-foreground sm:px-8 sm:text-sm">
|
||||
運用元:
|
||||
<a
|
||||
href="https://neody.land/ja"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-1 font-medium text-foreground underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Neodyland
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
return (
|
||||
<footer className="border-t bg-card/60">
|
||||
<div className="mx-auto flex w-full max-w-6xl items-center justify-center px-4 py-4 text-center text-xs text-muted-foreground sm:px-8 sm:text-sm">
|
||||
運用元:
|
||||
<a
|
||||
href="https://neody.land/ja"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-1 font-medium text-foreground underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Neodyland
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-40 border-b bg-background/85 backdrop-blur motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-top-2 motion-safe:duration-500">
|
||||
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-4 py-3 sm:px-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-sm font-semibold tracking-tight transition-colors duration-200 hover:text-primary sm:text-base"
|
||||
>
|
||||
Takasumi-Neodyマイクラサーバプロジェクト
|
||||
</Link>
|
||||
<nav className="flex items-center gap-4 text-sm">
|
||||
<Link
|
||||
href="/"
|
||||
className="underline-offset-4 transition-colors duration-200 hover:text-primary hover:underline"
|
||||
>
|
||||
メインページ
|
||||
</Link>
|
||||
<Link
|
||||
href="/announcements"
|
||||
className="underline-offset-4 transition-colors duration-200 hover:text-primary hover:underline"
|
||||
>
|
||||
お知らせ
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
return (
|
||||
<header className="sticky top-0 z-40 border-b bg-background/85 backdrop-blur motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-top-2 motion-safe:duration-500">
|
||||
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-4 py-3 sm:px-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-sm font-semibold tracking-tight transition-colors duration-200 hover:text-primary sm:text-base"
|
||||
>
|
||||
Takasumi-Neodyマイクラサーバプロジェクト
|
||||
</Link>
|
||||
<nav className="flex items-center gap-4 text-sm">
|
||||
<Link
|
||||
href="/"
|
||||
className="underline-offset-4 transition-colors duration-200 hover:text-primary hover:underline"
|
||||
>
|
||||
メインページ
|
||||
</Link>
|
||||
<Link
|
||||
href="/announcements"
|
||||
className="underline-offset-4 transition-colors duration-200 hover:text-primary hover:underline"
|
||||
>
|
||||
お知らせ
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
render,
|
||||
...props
|
||||
className,
|
||||
variant = "default",
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||
return useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
{
|
||||
className: cn(badgeVariants({ variant }), className),
|
||||
},
|
||||
props
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
})
|
||||
return useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
{
|
||||
className: cn(badgeVariants({ variant }), className),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
||||
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-9",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
||||
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-9",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@@ -1,103 +1,103 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-6 overflow-hidden rounded-xl bg-card py-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-6 overflow-hidden rounded-xl bg-card py-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
|
||||
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: SeparatorPrimitive.Props) {
|
||||
return (
|
||||
<SeparatorPrimitive
|
||||
data-slot="separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<SeparatorPrimitive
|
||||
data-slot="separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
||||
@@ -3,16 +3,16 @@ import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
@@ -6,88 +6,95 @@ import { markdownToHtml } from "@/lib/markdown";
|
||||
const ANNOUNCEMENTS_DIR = path.join(process.cwd(), "content", "announcements");
|
||||
|
||||
type AnnouncementFrontmatter = {
|
||||
title?: string;
|
||||
date?: string;
|
||||
summary?: string;
|
||||
title?: string;
|
||||
date?: string;
|
||||
summary?: string;
|
||||
};
|
||||
|
||||
export type AnnouncementListItem = {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
summary: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export type AnnouncementDetail = AnnouncementListItem & {
|
||||
contentHtml: string;
|
||||
contentHtml: string;
|
||||
};
|
||||
|
||||
function parseFrontmatter(markdown: string): {
|
||||
frontmatter: AnnouncementFrontmatter;
|
||||
body: string;
|
||||
frontmatter: AnnouncementFrontmatter;
|
||||
body: string;
|
||||
} {
|
||||
if (!markdown.startsWith("---\n")) {
|
||||
return { frontmatter: {}, body: markdown };
|
||||
}
|
||||
if (!markdown.startsWith("---\n")) {
|
||||
return { frontmatter: {}, body: markdown };
|
||||
}
|
||||
|
||||
const end = markdown.indexOf("\n---\n", 4);
|
||||
if (end === -1) {
|
||||
return { frontmatter: {}, body: markdown };
|
||||
}
|
||||
const end = markdown.indexOf("\n---\n", 4);
|
||||
if (end === -1) {
|
||||
return { frontmatter: {}, body: markdown };
|
||||
}
|
||||
|
||||
const rawFrontmatter = markdown.slice(4, end);
|
||||
const body = markdown.slice(end + 5);
|
||||
const frontmatter: AnnouncementFrontmatter = {};
|
||||
const rawFrontmatter = markdown.slice(4, end);
|
||||
const body = markdown.slice(end + 5);
|
||||
const frontmatter: AnnouncementFrontmatter = {};
|
||||
|
||||
for (const line of rawFrontmatter.split("\n")) {
|
||||
const delimiterIndex = line.indexOf(":");
|
||||
if (delimiterIndex <= 0) {
|
||||
continue;
|
||||
}
|
||||
for (const line of rawFrontmatter.split("\n")) {
|
||||
const delimiterIndex = line.indexOf(":");
|
||||
if (delimiterIndex <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, delimiterIndex).trim();
|
||||
const value = line.slice(delimiterIndex + 1).trim().replace(/^"|"$/g, "");
|
||||
const key = line.slice(0, delimiterIndex).trim();
|
||||
const value = line
|
||||
.slice(delimiterIndex + 1)
|
||||
.trim()
|
||||
.replace(/^"|"$/g, "");
|
||||
|
||||
if (key === "title" || key === "date" || key === "summary") {
|
||||
frontmatter[key] = value;
|
||||
}
|
||||
}
|
||||
if (key === "title" || key === "date" || key === "summary") {
|
||||
frontmatter[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { frontmatter, body: body.trim() };
|
||||
return { frontmatter, body: body.trim() };
|
||||
}
|
||||
|
||||
export async function getAnnouncementSlugs(): Promise<string[]> {
|
||||
const entries = await fs.readdir(ANNOUNCEMENTS_DIR, { withFileTypes: true });
|
||||
const entries = await fs.readdir(ANNOUNCEMENTS_DIR, { withFileTypes: true });
|
||||
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
||||
.map((entry) => entry.name.replace(/\.md$/, ""));
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
||||
.map((entry) => entry.name.replace(/\.md$/, ""));
|
||||
}
|
||||
|
||||
export async function getAnnouncementBySlug(slug: string): Promise<AnnouncementDetail> {
|
||||
const filePath = path.join(ANNOUNCEMENTS_DIR, `${slug}.md`);
|
||||
const markdown = await fs.readFile(filePath, "utf-8");
|
||||
const { frontmatter, body } = parseFrontmatter(markdown);
|
||||
export async function getAnnouncementBySlug(
|
||||
slug: string,
|
||||
): Promise<AnnouncementDetail> {
|
||||
const filePath = path.join(ANNOUNCEMENTS_DIR, `${slug}.md`);
|
||||
const markdown = await fs.readFile(filePath, "utf-8");
|
||||
const { frontmatter, body } = parseFrontmatter(markdown);
|
||||
|
||||
return {
|
||||
slug,
|
||||
title: frontmatter.title ?? slug,
|
||||
date: frontmatter.date ?? "日付未設定",
|
||||
summary: frontmatter.summary ?? "",
|
||||
contentHtml: markdownToHtml(body),
|
||||
};
|
||||
return {
|
||||
slug,
|
||||
title: frontmatter.title ?? slug,
|
||||
date: frontmatter.date ?? "日付未設定",
|
||||
summary: frontmatter.summary ?? "",
|
||||
contentHtml: markdownToHtml(body),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllAnnouncements(): Promise<AnnouncementListItem[]> {
|
||||
const slugs = await getAnnouncementSlugs();
|
||||
const items = await Promise.all(slugs.map((slug) => getAnnouncementBySlug(slug)));
|
||||
const slugs = await getAnnouncementSlugs();
|
||||
const items = await Promise.all(
|
||||
slugs.map((slug) => getAnnouncementBySlug(slug)),
|
||||
);
|
||||
|
||||
return items
|
||||
.map((item) => ({
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
date: item.date,
|
||||
summary: item.summary,
|
||||
}))
|
||||
.sort((a, b) => b.date.localeCompare(a.date));
|
||||
return items
|
||||
.map((item) => ({
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
date: item.date,
|
||||
summary: item.summary,
|
||||
}))
|
||||
.sort((a, b) => b.date.localeCompare(a.date));
|
||||
}
|
||||
|
||||
233
lib/markdown.ts
233
lib/markdown.ts
@@ -1,144 +1,155 @@
|
||||
type ParseState = {
|
||||
inCodeBlock: boolean;
|
||||
codeLang: string;
|
||||
paragraphBuffer: string[];
|
||||
listType: "ul" | "ol" | null;
|
||||
listBuffer: string[];
|
||||
inCodeBlock: boolean;
|
||||
codeLang: string;
|
||||
paragraphBuffer: string[];
|
||||
listType: "ul" | "ol" | null;
|
||||
listBuffer: string[];
|
||||
};
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function applyInlineMarkdown(value: string): string {
|
||||
const escaped = escapeHtml(value);
|
||||
const escaped = escapeHtml(value);
|
||||
|
||||
return escaped
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
||||
.replace(/~~([^~]+)~~/g, "<del>$1</del>")
|
||||
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
return escaped
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
||||
.replace(/~~([^~]+)~~/g, "<del>$1</del>")
|
||||
.replace(
|
||||
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
);
|
||||
}
|
||||
|
||||
function flushParagraph(state: ParseState, html: string[]): void {
|
||||
if (state.paragraphBuffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (state.paragraphBuffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
html.push(`<p>${applyInlineMarkdown(state.paragraphBuffer.join(" "))}</p>`);
|
||||
state.paragraphBuffer = [];
|
||||
html.push(`<p>${applyInlineMarkdown(state.paragraphBuffer.join(" "))}</p>`);
|
||||
state.paragraphBuffer = [];
|
||||
}
|
||||
|
||||
function flushList(state: ParseState, html: string[]): void {
|
||||
if (!state.listType || state.listBuffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!state.listType || state.listBuffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
html.push(`<${state.listType}>${state.listBuffer.join("")}</${state.listType}>`);
|
||||
state.listType = null;
|
||||
state.listBuffer = [];
|
||||
html.push(
|
||||
`<${state.listType}>${state.listBuffer.join("")}</${state.listType}>`,
|
||||
);
|
||||
state.listType = null;
|
||||
state.listBuffer = [];
|
||||
}
|
||||
|
||||
export function markdownToHtml(markdown: string): string {
|
||||
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
||||
const html: string[] = [];
|
||||
const state: ParseState = {
|
||||
inCodeBlock: false,
|
||||
codeLang: "",
|
||||
paragraphBuffer: [],
|
||||
listType: null,
|
||||
listBuffer: [],
|
||||
};
|
||||
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
||||
const html: string[] = [];
|
||||
const state: ParseState = {
|
||||
inCodeBlock: false,
|
||||
codeLang: "",
|
||||
paragraphBuffer: [],
|
||||
listType: null,
|
||||
listBuffer: [],
|
||||
};
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trimEnd();
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trimEnd();
|
||||
|
||||
if (line.startsWith("```")) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
if (line.startsWith("```")) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
|
||||
if (!state.inCodeBlock) {
|
||||
state.inCodeBlock = true;
|
||||
state.codeLang = line.slice(3).trim();
|
||||
const className = state.codeLang ? ` class="language-${escapeHtml(state.codeLang)}"` : "";
|
||||
html.push(`<pre><code${className}>`);
|
||||
} else {
|
||||
state.inCodeBlock = false;
|
||||
state.codeLang = "";
|
||||
html.push("</code></pre>");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!state.inCodeBlock) {
|
||||
state.inCodeBlock = true;
|
||||
state.codeLang = line.slice(3).trim();
|
||||
const className = state.codeLang
|
||||
? ` class="language-${escapeHtml(state.codeLang)}"`
|
||||
: "";
|
||||
html.push(`<pre><code${className}>`);
|
||||
} else {
|
||||
state.inCodeBlock = false;
|
||||
state.codeLang = "";
|
||||
html.push("</code></pre>");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state.inCodeBlock) {
|
||||
html.push(`${escapeHtml(rawLine)}\n`);
|
||||
continue;
|
||||
}
|
||||
if (state.inCodeBlock) {
|
||||
html.push(`${escapeHtml(rawLine)}\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.length === 0) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
continue;
|
||||
}
|
||||
if (line.length === 0) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
continue;
|
||||
}
|
||||
|
||||
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (headingMatch) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
const level = headingMatch[1].length;
|
||||
html.push(`<h${level}>${applyInlineMarkdown(headingMatch[2])}</h${level}>`);
|
||||
continue;
|
||||
}
|
||||
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (headingMatch) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
const level = headingMatch[1].length;
|
||||
html.push(
|
||||
`<h${level}>${applyInlineMarkdown(headingMatch[2])}</h${level}>`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^---$/.test(line)) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
html.push("<hr />");
|
||||
continue;
|
||||
}
|
||||
if (/^---$/.test(line)) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
html.push("<hr />");
|
||||
continue;
|
||||
}
|
||||
|
||||
const blockquoteMatch = line.match(/^>\s?(.*)$/);
|
||||
if (blockquoteMatch) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
html.push(`<blockquote><p>${applyInlineMarkdown(blockquoteMatch[1])}</p></blockquote>`);
|
||||
continue;
|
||||
}
|
||||
const blockquoteMatch = line.match(/^>\s?(.*)$/);
|
||||
if (blockquoteMatch) {
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
html.push(
|
||||
`<blockquote><p>${applyInlineMarkdown(blockquoteMatch[1])}</p></blockquote>`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ulMatch = line.match(/^[-*+]\s+(.+)$/);
|
||||
if (ulMatch) {
|
||||
flushParagraph(state, html);
|
||||
if (state.listType !== "ul") {
|
||||
flushList(state, html);
|
||||
state.listType = "ul";
|
||||
}
|
||||
state.listBuffer.push(`<li>${applyInlineMarkdown(ulMatch[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
const ulMatch = line.match(/^[-*+]\s+(.+)$/);
|
||||
if (ulMatch) {
|
||||
flushParagraph(state, html);
|
||||
if (state.listType !== "ul") {
|
||||
flushList(state, html);
|
||||
state.listType = "ul";
|
||||
}
|
||||
state.listBuffer.push(`<li>${applyInlineMarkdown(ulMatch[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const olMatch = line.match(/^\d+\.\s+(.+)$/);
|
||||
if (olMatch) {
|
||||
flushParagraph(state, html);
|
||||
if (state.listType !== "ol") {
|
||||
flushList(state, html);
|
||||
state.listType = "ol";
|
||||
}
|
||||
state.listBuffer.push(`<li>${applyInlineMarkdown(olMatch[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
const olMatch = line.match(/^\d+\.\s+(.+)$/);
|
||||
if (olMatch) {
|
||||
flushParagraph(state, html);
|
||||
if (state.listType !== "ol") {
|
||||
flushList(state, html);
|
||||
state.listType = "ol";
|
||||
}
|
||||
state.listBuffer.push(`<li>${applyInlineMarkdown(olMatch[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
flushList(state, html);
|
||||
state.paragraphBuffer.push(line.trim());
|
||||
}
|
||||
flushList(state, html);
|
||||
state.paragraphBuffer.push(line.trim());
|
||||
}
|
||||
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
flushParagraph(state, html);
|
||||
flushList(state, html);
|
||||
|
||||
return html.join("\n");
|
||||
return html.join("\n");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
trailingSlash: true,
|
||||
output: "export",
|
||||
trailingSlash: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
82
package.json
82
package.json
@@ -1,43 +1,43 @@
|
||||
{
|
||||
"name": "minecraft-server-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^1.7.0",
|
||||
"marked": "^17.0.5",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"shadcn": "^4.1.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"ignoreScripts": [
|
||||
"sharp",
|
||||
"unrs-resolver"
|
||||
],
|
||||
"trustedDependencies": [
|
||||
"sharp",
|
||||
"unrs-resolver"
|
||||
]
|
||||
"name": "minecraft-server-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^1.7.0",
|
||||
"marked": "^17.0.5",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"shadcn": "^4.1.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"ignoreScripts": [
|
||||
"sharp",
|
||||
"unrs-resolver"
|
||||
],
|
||||
"trustedDependencies": [
|
||||
"sharp",
|
||||
"unrs-resolver"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user