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