refactor: standardize code formatting across components and utilities
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:
2026-04-06 03:44:49 +00:00
parent 8624d2c805
commit 88233ff069
24 changed files with 935 additions and 869 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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. MinecraftJava版 </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. MinecraftJava版 </li>
<li>
2. Java版: マルチプレイ / 統合版:
サーバー
</li>
<li>3. </li>
<li>4. </li>
</ol>
</section>
</main>
</div>
);
}

View File

@@ -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
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"css": {
"parser": {
"tailwindDirectives": true
}
}
}

View File

@@ -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": {}
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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,
};

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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));
}

View File

@@ -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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
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");
}

View File

@@ -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));
}

View File

@@ -1,8 +1,8 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
trailingSlash: true,
output: "export",
trailingSlash: true,
};
export default nextConfig;

View File

@@ -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"
]
}

View File

@@ -1,7 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -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"]
}