feat: first commit

This commit is contained in:
2026-07-08 18:09:09 +02:00
parent a7cd7739ca
commit 82abc317f1
16 changed files with 1582 additions and 69 deletions
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KubernetesApiProvider">{}</component>
</project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 854 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+57
View File
@@ -0,0 +1,57 @@
import { minecraftServer } from "@/app/lib/server-config";
type McSrvStatPlayer = string | { name?: string; name_raw?: string };
type McSrvStatResponse = {
online?: boolean;
version?: string;
players?: {
online?: number;
max?: number;
list?: McSrvStatPlayer[];
};
};
export async function GET() {
try {
const response = await fetch(
`https://api.mcsrvstat.us/3/${minecraftServer.address}`,
{
next: { revalidate: 60 },
},
);
if (!response.ok) {
throw new Error(`Minecraft status API returned ${response.status}`);
}
const data = (await response.json()) as McSrvStatResponse;
return Response.json(
{
online: Boolean(data.online),
playersOnline: data.players?.online ?? null,
playersMax: data.players?.max ?? null,
version: data.version ?? null,
checkedAt: new Date().toISOString(),
},
{
headers: {
"Cache-Control": "s-maxage=45, stale-while-revalidate=120",
},
},
);
} catch {
return Response.json(
{
online: false,
playersOnline: null,
playersMax: null,
version: null,
checkedAt: new Date().toISOString(),
error: "Le statut du serveur est indisponible.",
},
{ status: 200 },
);
}
}
+46
View File
@@ -0,0 +1,46 @@
"use client";
import { useState } from "react";
type CopyAddressButtonProps = {
value: string;
className?: string;
};
export default function CopyAddressButton({
value,
className = "",
}: CopyAddressButtonProps) {
const [copied, setCopied] = useState(false);
async function copyAddress() {
try {
await navigator.clipboard.writeText(value);
setCopied(true);
window.setTimeout(() => setCopied(false), 1800);
} catch {
setCopied(false);
}
}
return (
<button
type="button"
className={`copy-address-button ${copied ? "is-copied" : ""} ${className}`}
onClick={copyAddress}
aria-live="polite"
>
<CopyIcon />
<span>{copied ? "Adresse copiée" : "Copier l'adresse"}</span>
</button>
);
}
function CopyIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M8 7.5A2.5 2.5 0 0 1 10.5 5h6A2.5 2.5 0 0 1 19 7.5v6A2.5 2.5 0 0 1 16.5 16h-6A2.5 2.5 0 0 1 8 13.5z" />
<path d="M5 10.5A2.5 2.5 0 0 1 7.5 8H8v5.5A2.5 2.5 0 0 0 10.5 16H16v.5a2.5 2.5 0 0 1-2.5 2.5h-6A2.5 2.5 0 0 1 5 16.5z" />
</svg>
);
}
+104
View File
@@ -0,0 +1,104 @@
import Link from "next/link";
import type { ReactNode } from "react";
type HomeElementProps = {
title: string;
description: string;
href: string;
actionLabel: string;
variant: "jellyfin" | "minecraft";
external?: boolean;
children: ReactNode;
};
export default function HomeElement({
title,
description,
href,
actionLabel,
variant,
external = false,
children,
}: HomeElementProps) {
const content = (
<>
<span className="service-status" aria-label="Service en ligne" />
<span className="service-icon-shell">{children}</span>
<span className="service-copy">
<strong>{title}</strong>
<span>{description}</span>
</span>
<span className="service-action">
{actionLabel}
<ArrowIcon />
</span>
</>
);
const className = `service-card service-card--${variant}`;
if (external) {
return (
<a href={href} className={className} target="_blank" rel="noopener noreferrer">
{content}
</a>
);
}
return (
<Link href={href} className={className}>
{content}
</Link>
);
}
export function JellyfinIcon() {
return (
<svg viewBox="0 0 96 96" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="jellyfin-gradient" x1="16" y1="84" x2="80" y2="12">
<stop stopColor="#7c3cff" />
<stop offset="0.52" stopColor="#00d8ff" />
<stop offset="1" stopColor="#c833ff" />
</linearGradient>
</defs>
<path
d="M48 11 88 80H8z"
fill="none"
stroke="url(#jellyfin-gradient)"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="9"
/>
<path
d="M48 35 66 67H30z"
fill="none"
stroke="url(#jellyfin-gradient)"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="8"
/>
</svg>
);
}
export function MinecraftIcon() {
return (
<svg viewBox="0 0 96 96" aria-hidden="true" focusable="false">
<path d="M48 8 84 28 48 48 12 28z" fill="#7bd452" />
<path d="M48 48 84 28v40L48 88z" fill="#6b4a2c" />
<path d="M48 48 12 28v40l36 20z" fill="#8a6238" />
<path d="M24 34h10v10H24zm19 9h11v11H43zm18-9h11v10H61z" fill="#5aa543" />
<path d="M56 58h9v10h-9zm-25 0h10v11H31zm14 14h9v10h-9z" fill="#4b3525" opacity=".7" />
</svg>
);
}
function ArrowIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M5 12h12" />
<path d="m13 6 6 6-6 6" />
</svg>
);
}
+121
View File
@@ -0,0 +1,121 @@
"use client";
import { useEffect, useState } from "react";
type ServerStatus = {
online: boolean;
playersOnline: number | null;
playersMax: number | null;
version: string | null;
checkedAt: string;
error?: string;
};
const loadingStatus: ServerStatus = {
online: false,
playersOnline: null,
playersMax: null,
version: null,
checkedAt: "",
};
export default function MinecraftStatus() {
const [status, setStatus] = useState<ServerStatus>(loadingStatus);
const [loading, setLoading] = useState(true);
useEffect(() => {
let alive = true;
async function refreshStatus() {
try {
const response = await fetch("/api/minecraft/status", {
cache: "no-store",
});
const payload = (await response.json()) as ServerStatus;
if (alive) {
setStatus(payload);
setLoading(false);
}
} catch {
if (alive) {
setStatus({
...loadingStatus,
checkedAt: new Date().toISOString(),
error: "Impossible de récupérer le statut.",
});
setLoading(false);
}
}
}
refreshStatus();
const interval = window.setInterval(refreshStatus, 60_000);
return () => {
alive = false;
window.clearInterval(interval);
};
}, []);
const playerCount =
status.playersOnline === null
? "-"
: `${status.playersOnline}${status.playersMax ? ` / ${status.playersMax}` : ""}`;
return (
<section id="statut" className="minecraft-section">
<div className="section-heading pixel-heading">
<h2>Statut du serveur</h2>
<p>Mis à jour automatiquement toutes les 60 secondes.</p>
</div>
<div className="status-panel block-panel">
<StatusItem
label="État"
value={loading ? "Vérification..." : status.online ? "En ligne" : "Hors ligne"}
tone={loading ? undefined : status.online ? "online" : "offline"}
/>
<StatusItem label="Joueurs en ligne" value={playerCount} />
<StatusItem label="Version" value={status.version ?? "-"} />
<StatusItem
label="Dernier check"
value={formatCheckedAt(status.checkedAt)}
/>
</div>
{status.error ? (
<p className="status-note">{status.error}</p>
) : null}
</section>
);
}
type StatusItemProps = {
label: string;
value: string;
tone?: "online" | "offline";
};
function StatusItem({ label, value, tone }: StatusItemProps) {
return (
<div className="status-item">
<span className="status-label">{label}</span>
<strong className={tone ? `status-value status-value--${tone}` : "status-value"}>
{tone ? <span className="status-light" aria-hidden="true" /> : null}
{value}
</strong>
</div>
);
}
function formatCheckedAt(value: string) {
if (!value) {
return "-";
}
return new Intl.DateTimeFormat("fr-FR", {
hour: "2-digit",
minute: "2-digit",
}).format(new Date(value));
}
+973 -7
View File
@@ -1,8 +1,21 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--background: #090a0f;
--foreground: #f8fbff;
--muted: #b4c0d5;
--surface: #141832;
--surface-strong: #1f2244;
--cyan: #00e7ff;
--violet: #8b4dff;
--pink: #ff4dd2;
--green: #33e879;
--gold: #ffd54a;
--stone: #252932;
--stone-light: #343a45;
--dirt: #5a3b22;
--paper: #f2efe3;
--shadow-hard: 6px 6px 0 rgb(0 0 0 / 55%);
}
@theme inline {
@@ -12,15 +25,968 @@
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
min-height: 100vh;
margin: 0;
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
}
svg {
display: block;
}
::selection {
background: var(--cyan);
color: #06111a;
}
*:focus-visible {
outline: 3px solid var(--gold);
outline-offset: 4px;
}
.stellar-page {
position: relative;
min-height: 100vh;
overflow: hidden;
background:
radial-gradient(ellipse at bottom, #1b2735 0%, #0d1021 48%, #05060c 100%);
}
.stellar-page::before,
.stellar-page::after {
position: fixed;
inset: 0;
z-index: 0;
content: "";
pointer-events: none;
}
.stellar-page::before {
background-image:
radial-gradient(circle, rgb(255 255 255 / 90%) 1px, transparent 1.5px),
radial-gradient(circle, rgb(0 231 255 / 65%) 1px, transparent 1.5px),
radial-gradient(circle, rgb(255 255 255 / 55%) 1.5px, transparent 2px);
background-position:
0 0,
34px 74px,
88px 12px;
background-size:
86px 86px,
142px 142px,
230px 230px;
animation: star-drift 120s linear infinite;
}
.stellar-page::after {
background:
linear-gradient(180deg, transparent 0%, rgb(0 231 255 / 6%) 58%, rgb(51 232 121 / 10%) 100%),
radial-gradient(ellipse at 30% 80%, rgb(139 77 255 / 18%), transparent 38%),
radial-gradient(ellipse at 80% 70%, rgb(0 231 255 / 13%), transparent 32%);
}
.home-shell {
position: relative;
z-index: 1;
display: flex;
min-height: 100vh;
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 32px 0 22px;
flex-direction: column;
align-items: center;
}
.home-nav {
display: flex;
width: 100%;
justify-content: center;
}
.brand-lockup,
.minecraft-brand {
display: inline-flex;
align-items: center;
gap: 12px;
font-weight: 800;
}
.brand-lockup {
color: #a8f5ff;
font-family: var(--font-fredoka), var(--font-geist-sans), sans-serif;
font-size: 1.2rem;
text-shadow: 0 0 18px rgb(0 231 255 / 45%);
}
.brand-mark {
display: grid;
width: 54px;
height: 54px;
place-items: center;
border: 2px solid rgb(168 245 255 / 85%);
border-radius: 8px;
background: rgb(7 20 36 / 72%);
box-shadow: 0 0 22px rgb(0 231 255 / 35%), inset 0 0 14px rgb(0 231 255 / 20%);
color: #a8f5ff;
font-size: 2rem;
line-height: 1;
}
.home-hero {
max-width: 920px;
margin-top: 54px;
text-align: center;
}
.home-hero h1 {
margin: 0;
font-family: var(--font-fredoka), var(--font-geist-sans), sans-serif;
font-size: 4.8rem;
font-weight: 700;
line-height: 0.98;
text-shadow: 0 10px 0 rgb(0 0 0 / 32%), 0 0 26px rgb(255 255 255 / 20%);
}
.home-hero p {
max-width: 720px;
margin: 22px auto 0;
color: #c7d0e6;
font-family: var(--font-fredoka), var(--font-geist-sans), sans-serif;
font-size: 1.45rem;
font-weight: 600;
line-height: 1.45;
}
.service-grid {
display: grid;
width: min(860px, 100%);
margin-top: 48px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 32px;
}
.service-card {
--service-accent: var(--cyan);
position: relative;
display: flex;
min-height: 314px;
padding: 32px;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
border: 3px solid var(--cyan);
border-radius: 8px;
background:
linear-gradient(180deg, rgb(20 24 50 / 88%), rgb(8 10 22 / 92%)),
radial-gradient(circle at 50% 20%, rgb(255 255 255 / 10%), transparent 34%);
box-shadow: var(--shadow-hard), inset 0 0 0 1px rgb(255 255 255 / 9%);
color: var(--foreground);
text-align: center;
transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
}
.service-card::before {
position: absolute;
inset: 20px;
content: "";
background-image:
radial-gradient(circle, currentColor 1.5px, transparent 2px),
radial-gradient(circle, currentColor 1px, transparent 1.5px);
background-position:
0 0,
26px 34px;
background-size:
52px 52px,
74px 74px;
opacity: 0.18;
z-index: 0;
}
.service-card > * {
position: relative;
z-index: 1;
}
.service-card:hover {
transform: translateY(-8px);
}
.service-card--jellyfin {
--service-accent: var(--violet);
border-color: var(--violet);
box-shadow: var(--shadow-hard), 0 0 28px rgb(139 77 255 / 34%), inset 0 0 0 1px rgb(255 255 255 / 9%);
color: #ae77ff;
}
.service-card--minecraft {
--service-accent: var(--green);
border-color: var(--green);
box-shadow: var(--shadow-hard), 0 0 28px rgb(51 232 121 / 28%), inset 0 0 0 1px rgb(255 255 255 / 9%);
color: #46ee89;
}
.service-status {
position: absolute;
top: 24px;
right: 24px;
width: 18px;
height: 18px;
border: 3px solid rgb(3 17 18 / 72%);
border-radius: 999px;
background: var(--green);
box-shadow: 0 0 0 4px rgb(51 232 121 / 18%), 0 0 18px rgb(51 232 121 / 80%);
}
.service-icon-shell {
position: relative;
display: grid;
width: 116px;
height: 116px;
place-items: center;
margin-bottom: 22px;
}
.service-icon-shell svg {
width: 94px;
height: 94px;
filter: drop-shadow(0 8px 14px rgb(0 0 0 / 40%));
}
.service-copy {
position: relative;
display: grid;
gap: 10px;
width: 100%;
padding-top: 22px;
}
.service-copy::before {
position: absolute;
top: 0;
left: 18%;
right: 18%;
height: 3px;
background: currentColor;
box-shadow: 0 0 12px currentColor;
content: "";
opacity: 0.6;
}
.service-copy strong {
color: #fff;
font-family: var(--font-fredoka), var(--font-geist-sans), sans-serif;
font-size: 2.05rem;
line-height: 1;
text-shadow: 0 4px 0 rgb(0 0 0 / 40%);
}
.service-copy span {
min-height: 48px;
color: #bdc8dd;
font-size: 1rem;
line-height: 1.45;
}
.service-action {
display: inline-flex;
min-height: 56px;
width: 100%;
margin-top: 24px;
align-items: center;
justify-content: center;
gap: 12px;
border: 2px solid rgb(255 255 255 / 20%);
border-radius: 8px;
background: var(--service-accent);
color: #06111a;
font-weight: 800;
}
.service-card--jellyfin .service-action {
background: linear-gradient(180deg, #a95cff, #7d3cff);
color: #fff;
text-shadow: 0 2px 0 rgb(0 0 0 / 24%);
}
.service-card--minecraft .service-action {
background: linear-gradient(180deg, #35f48a, #13ca69);
color: #041411;
}
.service-action svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.5;
}
.home-footer-hint {
display: flex;
width: min(560px, 100%);
margin-top: auto;
padding-top: 42px;
align-items: center;
justify-content: center;
gap: 24px;
color: #a6b1cc;
font-family: var(--font-fredoka), var(--font-geist-sans), sans-serif;
font-size: 1.1rem;
}
.home-footer-hint span {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--cyan);
box-shadow: 0 0 14px var(--cyan);
}
.home-footer-hint span:first-child {
background: var(--violet);
box-shadow: 0 0 14px var(--violet);
}
.minecraft-page {
min-height: 100vh;
overflow-x: hidden;
background:
linear-gradient(180deg, #0c1220 0%, #11161d 42%, #1e140e 100%);
color: var(--paper);
font-family: var(--font-retro), var(--font-geist-mono), monospace;
font-size: 1.35rem;
line-height: 1.35;
}
.minecraft-nav {
position: sticky;
top: 0;
z-index: 30;
display: flex;
width: min(1120px, calc(100% - 24px));
margin: 12px auto 0;
align-items: center;
justify-content: space-between;
gap: 16px;
border: 4px solid #030303;
background:
linear-gradient(135deg, rgb(255 255 255 / 5%) 25%, transparent 25%) 0 0 / 22px 22px,
linear-gradient(315deg, rgb(0 0 0 / 22%) 25%, transparent 25%) 0 0 / 22px 22px,
rgb(37 41 50 / 95%);
box-shadow: var(--shadow-hard);
backdrop-filter: blur(6px);
}
.minecraft-brand {
padding: 14px 18px;
color: var(--paper);
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 0.78rem;
}
.minecraft-brand svg,
.minecraft-nav-links svg {
width: 26px;
height: 26px;
fill: var(--diamond, #5ad4e6);
stroke: #0a0c12;
stroke-width: 1.1;
}
.minecraft-nav-links {
display: flex;
align-items: stretch;
}
.minecraft-nav-links a {
display: flex;
min-height: 56px;
padding: 0 18px;
align-items: center;
border-left: 2px solid rgb(0 0 0 / 48%);
color: #f5f1e8;
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 0.66rem;
transition: background 160ms ease, color 160ms ease;
}
.minecraft-nav-links a:hover {
background: rgb(255 213 74 / 12%);
color: var(--gold);
}
.minecraft-hero {
position: relative;
display: grid;
min-height: 680px;
margin-top: -72px;
padding: 148px 24px 86px;
place-items: center;
overflow: hidden;
}
.minecraft-hero::after {
position: absolute;
inset: auto 0 0;
height: 210px;
background: linear-gradient(180deg, transparent, rgb(12 18 32 / 80%) 62%, #11161d 100%);
content: "";
}
.minecraft-hero-image {
z-index: 0;
object-fit: cover;
filter: saturate(1.05) contrast(1.04);
}
.minecraft-hero-content {
position: relative;
z-index: 2;
display: flex;
width: min(860px, 100%);
flex-direction: column;
align-items: center;
text-align: center;
text-shadow: 4px 4px 0 #030303;
}
.minecraft-hero-content h1 {
margin: 0;
color: #fff;
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 4rem;
line-height: 1.18;
text-transform: uppercase;
}
.minecraft-hero-content p {
max-width: 660px;
margin: 22px 0 26px;
color: #f4f1e8;
font-size: 1.8rem;
}
.block-panel {
border: 4px solid #030303;
background:
linear-gradient(135deg, rgb(255 255 255 / 5%) 25%, transparent 25%) 0 0 / 24px 24px,
linear-gradient(315deg, rgb(0 0 0 / 20%) 25%, transparent 25%) 0 0 / 24px 24px,
var(--stone);
box-shadow:
inset -4px -4px 0 rgb(0 0 0 / 45%),
inset 4px 4px 0 rgb(255 255 255 / 8%),
var(--shadow-hard);
}
.server-address {
display: inline-flex;
max-width: 100%;
padding: 18px 24px;
align-items: center;
justify-content: center;
gap: 16px;
color: var(--cyan);
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 1.35rem;
overflow-wrap: anywhere;
text-shadow: none;
}
.server-address svg {
width: 34px;
min-width: 34px;
height: 34px;
fill: #56e1ef;
stroke: #041314;
stroke-width: 1.4;
}
.minecraft-actions {
display: flex;
margin-top: 22px;
flex-wrap: wrap;
justify-content: center;
gap: 18px;
text-shadow: none;
}
.copy-address-button,
.map-button,
.large-map-link,
.back-home-link {
display: inline-flex;
min-height: 58px;
align-items: center;
justify-content: center;
gap: 12px;
border: 4px solid #030303;
box-shadow:
inset -3px -3px 0 rgb(0 0 0 / 32%),
inset 3px 3px 0 rgb(255 255 255 / 22%),
5px 5px 0 rgb(0 0 0 / 48%);
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 0.82rem;
line-height: 1.25;
transition: transform 160ms ease, box-shadow 160ms ease;
}
.copy-address-button:hover,
.map-button:hover,
.large-map-link:hover,
.back-home-link:hover {
transform: translateY(-4px);
box-shadow:
inset -3px -3px 0 rgb(0 0 0 / 32%),
inset 3px 3px 0 rgb(255 255 255 / 22%),
8px 8px 0 rgb(0 0 0 / 56%);
}
.copy-address-button {
padding: 0 24px;
background: var(--gold);
color: #16120a;
cursor: pointer;
}
.copy-address-button.is-copied {
background: var(--green);
}
.map-button,
.large-map-link {
padding: 0 24px;
background: #12c8c7;
color: #061115;
}
.copy-address-button svg,
.map-button svg,
.large-map-link svg,
.back-home-link svg {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2;
}
.minecraft-content {
position: relative;
z-index: 3;
width: min(1120px, calc(100% - 32px));
margin: -48px auto 0;
padding-bottom: 80px;
}
.minecraft-section {
scroll-margin-top: 100px;
margin-top: 56px;
}
.section-heading {
margin-bottom: 22px;
text-align: center;
}
.pixel-heading h2,
h2.pixel-heading {
margin: 0;
color: #fff;
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 1.45rem;
line-height: 1.35;
text-shadow: 3px 3px 0 #030303;
text-transform: uppercase;
}
.section-heading p,
.map-copy p {
margin: 8px 0 0;
color: #b8c2cf;
font-size: 1.45rem;
}
.status-panel {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
padding: 22px;
}
.status-item {
display: grid;
min-height: 92px;
padding: 12px 20px;
place-items: center;
border-right: 2px solid rgb(255 255 255 / 12%);
text-align: center;
}
.status-item:last-child {
border-right: 0;
}
.status-label {
color: #c7cbd1;
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 0.68rem;
text-transform: uppercase;
}
.status-value {
display: inline-flex;
max-width: 100%;
align-items: center;
justify-content: center;
gap: 10px;
color: #fff;
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 0.88rem;
overflow-wrap: anywhere;
}
.status-value--online {
color: #7aff5f;
}
.status-value--offline {
color: #ff5c5c;
}
.status-light {
width: 16px;
height: 16px;
background: currentColor;
box-shadow: 0 0 14px currentColor;
}
.status-note {
margin: 18px 0 0;
color: #ffce86;
text-align: center;
}
.steps-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 22px;
}
.step-card {
position: relative;
min-height: 230px;
padding: 58px 22px 24px;
}
.step-number {
position: absolute;
top: 16px;
left: 16px;
display: grid;
width: 38px;
height: 38px;
place-items: center;
border: 3px solid #030303;
background: #080a0f;
color: var(--gold);
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 1rem;
}
.step-card h3 {
margin: 0 0 12px;
color: var(--gold);
font-family: var(--font-pixel), var(--font-geist-mono), monospace;
font-size: 0.78rem;
line-height: 1.45;
text-transform: uppercase;
}
.step-card p {
margin: 0;
color: #d6d1c4;
font-size: 1.32rem;
}
.map-section {
display: grid;
grid-template-columns: 0.85fr 1fr;
gap: 28px;
align-items: stretch;
}
.map-preview {
min-height: 250px;
padding: 18px;
background:
linear-gradient(135deg, rgb(255 255 255 / 9%) 25%, transparent 25%) 0 0 / 20px 20px,
#24180f;
}
.map-island {
position: relative;
width: 100%;
height: 100%;
min-height: 214px;
border: 4px solid #120b06;
background:
linear-gradient(45deg, transparent 46%, rgb(0 0 0 / 18%) 48%, rgb(0 0 0 / 18%) 52%, transparent 54%) 0 0 / 34px 34px,
radial-gradient(circle at 24% 42%, #7fbf4d 0 9%, transparent 10%),
radial-gradient(circle at 52% 35%, #8bc95a 0 12%, transparent 13%),
radial-gradient(circle at 71% 58%, #6ead43 0 10%, transparent 11%),
linear-gradient(135deg, #2d8ac7 0 36%, #75b756 36% 64%, #2d8ac7 64% 100%);
image-rendering: pixelated;
}
.map-pin {
position: absolute;
top: 22%;
right: 18%;
width: 30px;
height: 30px;
border: 4px solid #160302;
border-radius: 50% 50% 50% 0;
background: #ff493f;
transform: rotate(-45deg);
box-shadow: 4px 4px 0 rgb(0 0 0 / 38%);
}
.map-copy {
display: flex;
padding: 34px;
flex-direction: column;
justify-content: center;
border: 4px solid #030303;
background: rgb(37 41 50 / 92%);
box-shadow: var(--shadow-hard);
}
.large-map-link {
width: 100%;
margin-top: 24px;
justify-content: space-between;
font-size: 1rem;
}
.minecraft-back {
display: flex;
margin-top: 42px;
justify-content: center;
}
.back-home-link {
padding: 0 24px;
background: var(--stone-light);
color: var(--paper);
}
@keyframes star-drift {
from {
transform: translateY(0);
}
to {
transform: translateY(-220px);
}
}
@media (max-width: 960px) {
.home-hero h1 {
font-size: 3.6rem;
}
.service-grid {
gap: 22px;
}
.minecraft-nav {
position: relative;
flex-direction: column;
align-items: stretch;
}
.minecraft-brand {
justify-content: center;
}
.minecraft-nav-links {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.minecraft-nav-links a {
min-height: 48px;
justify-content: center;
border-top: 2px solid rgb(0 0 0 / 48%);
border-left: 0;
}
.minecraft-hero {
margin-top: -118px;
padding-top: 214px;
}
.status-panel,
.steps-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.status-item:nth-child(2) {
border-right: 0;
}
.status-item:nth-child(-n + 2) {
border-bottom: 2px solid rgb(255 255 255 / 12%);
}
.map-section {
grid-template-columns: 1fr;
}
}
@media (max-width: 700px) {
.home-shell {
width: min(100% - 24px, 520px);
padding-top: 24px;
}
.brand-lockup {
font-size: 1rem;
}
.brand-mark {
width: 46px;
height: 46px;
font-size: 1.6rem;
}
.home-hero {
margin-top: 42px;
}
.home-hero h1 {
font-size: 3rem;
}
.home-hero p {
font-size: 1.18rem;
}
.service-grid {
grid-template-columns: 1fr;
margin-top: 34px;
}
.service-card {
min-height: 276px;
padding: 28px;
}
.home-footer-hint {
padding-top: 30px;
}
.minecraft-page {
font-size: 1.2rem;
}
.minecraft-nav-links {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.minecraft-nav-links a {
font-size: 0.58rem;
}
.minecraft-hero {
min-height: 720px;
padding-right: 16px;
padding-left: 16px;
}
.minecraft-hero-content h1 {
font-size: 2.45rem;
}
.minecraft-hero-content p {
font-size: 1.42rem;
}
.server-address {
padding: 16px;
font-size: 0.88rem;
}
.minecraft-actions,
.copy-address-button,
.map-button {
width: 100%;
}
.status-panel,
.steps-grid {
grid-template-columns: 1fr;
}
.status-item,
.status-item:nth-child(2),
.status-item:nth-child(-n + 2) {
border-right: 0;
border-bottom: 2px solid rgb(255 255 255 / 12%);
}
.status-item:last-child {
border-bottom: 0;
}
.pixel-heading h2,
h2.pixel-heading {
font-size: 1.05rem;
}
.section-heading p,
.map-copy p {
font-size: 1.24rem;
}
.large-map-link {
font-size: 0.72rem;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
+23 -5
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Fredoka, Geist, Geist_Mono, Press_Start_2P, VT323 } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
@@ -12,9 +12,26 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
const fredoka = Fredoka({
variable: "--font-fredoka",
subsets: ["latin"],
});
const pressStart = Press_Start_2P({
variable: "--font-pixel",
subsets: ["latin"],
weight: "400",
});
const vt323 = VT323({
variable: "--font-retro",
subsets: ["latin"],
weight: "400",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Le serveur de Leon",
description: "Streaming, Minecraft et services privés de Leon.",
};
export default function RootLayout({
@@ -24,8 +41,9 @@ export default function RootLayout({
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
lang="fr"
data-scroll-behavior="smooth"
className={`${geistSans.variable} ${geistMono.variable} ${fredoka.variable} ${pressStart.variable} ${vt323.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
export const siteConfig = {
name: "leonmorival.xyz",
title: "Le serveur de Leon",
description: "Streaming, jeux et services privés au même endroit.",
jellyfinUrl: "https://momostreaming.com",
minecraftPath: "/minecraft",
};
export const minecraftServer = {
name: "Minecraft Survie",
address: "minecraft.leonmorival.xyz",
mapUrl: "https://map.leonmorival.xyz",
versionHint: "Java",
access: "Whitelist sur demande",
};
+182
View File
@@ -0,0 +1,182 @@
import Image from "next/image";
import Link from "next/link";
import CopyAddressButton from "../components/CopyAddressButton";
import MinecraftStatus from "../components/MinecraftStatus";
import { minecraftServer, siteConfig } from "../lib/server-config";
const joinSteps = [
{
number: "1",
title: "Lance Minecraft Java",
text: "Utilise ton launcher habituel avec la version Java recommandée par le serveur.",
},
{
number: "2",
title: "Ajoute le serveur",
text: `Renseigne ${minecraftServer.address} dans la liste multijoueur.`,
},
{
number: "3",
title: "Demande l'accès",
text: "Le serveur fonctionne avec une whitelist pour garder un espace privé.",
},
{
number: "4",
title: "Explore la map",
text: "Une fois connecté, retrouve les constructions sur la carte live.",
},
];
export default function MinecraftPage() {
return (
<main className="minecraft-page">
<nav className="minecraft-nav" aria-label="Navigation Minecraft">
<Link href="/" className="minecraft-brand">
<SwordIcon />
<span>{siteConfig.name}</span>
</Link>
<div className="minecraft-nav-links">
<Link href="/">Accueil</Link>
<a href="#statut">Statut</a>
<a href="#rejoindre">Rejoindre</a>
<a href={minecraftServer.mapUrl} target="_blank" rel="noopener noreferrer">
Carte
</a>
</div>
</nav>
<section className="minecraft-hero" aria-labelledby="minecraft-title">
<Image
src="/assets/minecraft-night.png"
alt=""
fill
sizes="100vw"
priority
className="minecraft-hero-image"
/>
<div className="minecraft-hero-content">
<h1 id="minecraft-title">Minecraft Survie</h1>
<p>Un monde privé pour construire, explorer et jouer entre amis.</p>
<div className="server-address block-panel">
<DiamondIcon />
<span>{minecraftServer.address}</span>
</div>
<div className="minecraft-actions">
<CopyAddressButton value={minecraftServer.address} />
<a
className="map-button"
href={minecraftServer.mapUrl}
target="_blank"
rel="noopener noreferrer"
>
<MapIcon />
<span>Ouvrir la map</span>
</a>
</div>
</div>
</section>
<div className="minecraft-content">
<MinecraftStatus />
<section id="rejoindre" className="minecraft-section">
<div className="section-heading pixel-heading">
<h2>Comment rejoindre</h2>
<p>Quatre étapes pour entrer dans le monde.</p>
</div>
<div className="steps-grid">
{joinSteps.map((step) => (
<article className="step-card block-panel" key={step.number}>
<span className="step-number">{step.number}</span>
<h3>{step.title}</h3>
<p>{step.text}</p>
</article>
))}
</div>
</section>
<section className="minecraft-section map-section" aria-labelledby="map-title">
<div className="map-preview block-panel" aria-hidden="true">
<div className="map-island">
<span className="map-pin" />
</div>
</div>
<div className="map-copy">
<h2 id="map-title" className="pixel-heading">
Carte live
</h2>
<p>Explore le monde en temps réel depuis ton navigateur.</p>
<a
className="large-map-link"
href={minecraftServer.mapUrl}
target="_blank"
rel="noopener noreferrer"
>
<MapIcon />
<span>map.leonmorival.xyz</span>
<ArrowIcon />
</a>
</div>
</section>
<div className="minecraft-back">
<Link href="/" className="back-home-link">
<ArrowLeftIcon />
<span>Retour accueil</span>
</Link>
</div>
</div>
</main>
);
}
function SwordIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M14.6 4h5.4v5.4L9 20.4 3.6 15z" />
<path d="M5.8 12.8 11.2 18" />
<path d="M4 20 8 16" />
</svg>
);
}
function DiamondIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m12 2 8 8-8 12-8-12z" />
<path d="M4 10h16" />
<path d="m8 10 4 12 4-12" />
</svg>
);
}
function MapIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m3 6 6-2 6 2 6-2v14l-6 2-6-2-6 2z" />
<path d="M9 4v14" />
<path d="M15 6v14" />
</svg>
);
}
function ArrowIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M5 12h12" />
<path d="m13 6 6 6-6 6" />
</svg>
);
}
function ArrowLeftIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M19 12H7" />
<path d="m11 6-6 6 6 6" />
</svg>
);
}
+45 -55
View File
@@ -1,65 +1,55 @@
import Image from "next/image";
import Link from "next/link";
import HomeElement, {
JellyfinIcon,
MinecraftIcon,
} from "./components/HomeElement";
import { siteConfig } from "./lib/server-config";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<main className="stellar-page home-page">
<section className="home-shell" aria-labelledby="home-title">
<nav className="home-nav" aria-label="Navigation principale">
<Link href="/" className="brand-lockup" aria-label="Accueil">
<span className="brand-mark">L</span>
<span>{siteConfig.name}</span>
</Link>
</nav>
<div className="home-hero">
<h1 id="home-title">{siteConfig.title}</h1>
<p>{siteConfig.description}</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
<div className="service-grid" aria-label="Services disponibles">
<HomeElement
title="Jellyfin"
description="Films, séries et bibliothèque privée."
href={siteConfig.jellyfinUrl}
actionLabel="Ouvrir"
variant="jellyfin"
external
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
<JellyfinIcon />
</HomeElement>
<HomeElement
title="Minecraft"
description="Infos, statut et carte live du monde."
href={siteConfig.minecraftPath}
actionLabel="Voir le serveur"
variant="minecraft"
>
Documentation
</a>
<MinecraftIcon />
</HomeElement>
</div>
<div className="home-footer-hint" aria-hidden="true">
<span />
<strong>Services privés</strong>
<span />
</div>
</section>
</main>
</div>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB