diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..90dee70 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + {} + \ No newline at end of file diff --git a/.tmp/qa/home-desktop.png b/.tmp/qa/home-desktop.png new file mode 100644 index 0000000..a50dfb8 Binary files /dev/null and b/.tmp/qa/home-desktop.png differ diff --git a/.tmp/qa/home-mobile.png b/.tmp/qa/home-mobile.png new file mode 100644 index 0000000..0f303aa Binary files /dev/null and b/.tmp/qa/home-mobile.png differ diff --git a/.tmp/qa/minecraft-desktop.png b/.tmp/qa/minecraft-desktop.png new file mode 100644 index 0000000..c6a392e Binary files /dev/null and b/.tmp/qa/minecraft-desktop.png differ diff --git a/.tmp/qa/minecraft-mobile.png b/.tmp/qa/minecraft-mobile.png new file mode 100644 index 0000000..26d3072 Binary files /dev/null and b/.tmp/qa/minecraft-mobile.png differ diff --git a/app/api/minecraft/status/route.ts b/app/api/minecraft/status/route.ts new file mode 100644 index 0000000..3d7f6c5 --- /dev/null +++ b/app/api/minecraft/status/route.ts @@ -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 }, + ); + } +} diff --git a/app/components/CopyAddressButton.tsx b/app/components/CopyAddressButton.tsx new file mode 100644 index 0000000..e964d04 --- /dev/null +++ b/app/components/CopyAddressButton.tsx @@ -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 ( + + ); +} + +function CopyIcon() { + return ( + + ); +} diff --git a/app/components/HomeElement.tsx b/app/components/HomeElement.tsx new file mode 100644 index 0000000..03285d7 --- /dev/null +++ b/app/components/HomeElement.tsx @@ -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 = ( + <> + + {children} + + {title} + {description} + + + {actionLabel} + + + + ); + + const className = `service-card service-card--${variant}`; + + if (external) { + return ( + + {content} + + ); + } + + return ( + + {content} + + ); +} + +export function JellyfinIcon() { + return ( + + ); +} + +export function MinecraftIcon() { + return ( + + ); +} + +function ArrowIcon() { + return ( + + ); +} diff --git a/app/components/MinecraftStatus.tsx b/app/components/MinecraftStatus.tsx new file mode 100644 index 0000000..3ee2a71 --- /dev/null +++ b/app/components/MinecraftStatus.tsx @@ -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(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 ( +
+
+

Statut du serveur

+

Mis à jour automatiquement toutes les 60 secondes.

+
+ +
+ + + + +
+ + {status.error ? ( +

{status.error}

+ ) : null} +
+ ); +} + +type StatusItemProps = { + label: string; + value: string; + tone?: "online" | "offline"; +}; + +function StatusItem({ label, value, tone }: StatusItemProps) { + return ( +
+ {label} + + {tone ? +
+ ); +} + +function formatCheckedAt(value: string) { + if (!value) { + return "-"; + } + + return new Intl.DateTimeFormat("fr-FR", { + hour: "2-digit", + minute: "2-digit", + }).format(new Date(value)); +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..9087e33 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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; + } } diff --git a/app/layout.tsx b/app/layout.tsx index 976eb90..075ebee 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( {children} diff --git a/app/lib/server-config.ts b/app/lib/server-config.ts new file mode 100644 index 0000000..c049b85 --- /dev/null +++ b/app/lib/server-config.ts @@ -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", +}; diff --git a/app/minecraft/page.tsx b/app/minecraft/page.tsx new file mode 100644 index 0000000..9cd2576 --- /dev/null +++ b/app/minecraft/page.tsx @@ -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 ( +
+ + +
+ +
+

Minecraft Survie

+

Un monde privé pour construire, explorer et jouer entre amis.

+ +
+ + {minecraftServer.address} +
+ + +
+
+ +
+ + +
+
+

Comment rejoindre

+

Quatre étapes pour entrer dans le monde.

+
+ +
+ {joinSteps.map((step) => ( +
+ {step.number} +

{step.title}

+

{step.text}

+
+ ))} +
+
+ +
+ +
+

+ Carte live +

+

Explore le monde en temps réel depuis ton navigateur.

+ + + map.leonmorival.xyz + + +
+
+ +
+ + + Retour accueil + +
+
+
+ ); +} + +function SwordIcon() { + return ( + + ); +} + +function DiamondIcon() { + return ( + + ); +} + +function MapIcon() { + return ( + + ); +} + +function ArrowIcon() { + return ( + + ); +} + +function ArrowLeftIcon() { + return ( + + ); +} diff --git a/app/page.tsx b/app/page.tsx index 3f36f7c..ea8af9f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+
+ + +
+

{siteConfig.title}

+

{siteConfig.description}

- -
-
+ + + +
); } diff --git a/public/assets/minecraft-night.png b/public/assets/minecraft-night.png new file mode 100644 index 0000000..28dbabe Binary files /dev/null and b/public/assets/minecraft-night.png differ