(Feat) Init WebSite
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
"use server";
|
||||
|
||||
export type ContactState = {
|
||||
status: "idle" | "success" | "error";
|
||||
message: string;
|
||||
fieldErrors?: Partial<Record<"name" | "email" | "subject" | "message", string>>;
|
||||
};
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export async function sendContact(
|
||||
_prev: ContactState,
|
||||
formData: FormData,
|
||||
): Promise<ContactState> {
|
||||
const name = String(formData.get("name") ?? "").trim();
|
||||
const email = String(formData.get("email") ?? "").trim();
|
||||
const subject = String(formData.get("subject") ?? "").trim();
|
||||
const message = String(formData.get("message") ?? "").trim();
|
||||
const honeypot = String(formData.get("website") ?? "");
|
||||
|
||||
if (honeypot) {
|
||||
return { status: "success", message: "Thanks for reaching out." };
|
||||
}
|
||||
|
||||
const fieldErrors: ContactState["fieldErrors"] = {};
|
||||
if (!name) fieldErrors.name = "Required.";
|
||||
if (!email || !EMAIL_RE.test(email)) fieldErrors.email = "Valid email required.";
|
||||
if (!subject) fieldErrors.subject = "Required.";
|
||||
if (!message || message.length < 10) {
|
||||
fieldErrors.message = "Tell us a bit more (10+ chars).";
|
||||
}
|
||||
|
||||
if (Object.keys(fieldErrors).length > 0) {
|
||||
return {
|
||||
status: "error",
|
||||
message: "Please fix the highlighted fields.",
|
||||
fieldErrors,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: wire to Resend / Postmark / SendGrid to actually deliver the email.
|
||||
// For now we log so messages are visible in server logs.
|
||||
console.log("[contact]", { name, email, subject, message });
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
message: "Message sent. We'll get back to you soon.",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use server";
|
||||
|
||||
export type SubscribeState = {
|
||||
status: "idle" | "success" | "error";
|
||||
message: string;
|
||||
};
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export async function subscribe(
|
||||
_prev: SubscribeState,
|
||||
formData: FormData,
|
||||
): Promise<SubscribeState> {
|
||||
const email = String(formData.get("email") ?? "").trim();
|
||||
const honeypot = String(formData.get("website") ?? "");
|
||||
|
||||
if (honeypot) {
|
||||
return { status: "success", message: "Thanks — we'll keep you posted." };
|
||||
}
|
||||
|
||||
if (!email || !EMAIL_RE.test(email)) {
|
||||
return {
|
||||
status: "error",
|
||||
message: "Please enter a valid email address.",
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: wire to a real provider (Buttondown, ConvertKit, Mailchimp, Resend Audiences).
|
||||
// For now we just log the signup so you can collect addresses from server logs.
|
||||
console.log(`[newsletter] new signup: ${email}`);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
message: "Welcome to the climb. Check your inbox soon.",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState } from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { sendContact, type ContactState } from "@/app/actions/contact";
|
||||
|
||||
const initialState: ContactState = { status: "idle", message: "" };
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="px-8 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground hover:scale-[1.03] transition duration-200 disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
>
|
||||
{pending ? "SENDING…" : "SEND MESSAGE"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const inputBase =
|
||||
"w-full bg-surface border px-4 py-3 text-sm text-foreground placeholder:text-foreground-muted focus:outline-none transition-colors";
|
||||
|
||||
export function ContactForm() {
|
||||
const [state, formAction] = useActionState(sendContact, initialState);
|
||||
|
||||
if (state.status === "success") {
|
||||
return (
|
||||
<div className="border border-accent/40 bg-surface p-8 text-center">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-3">
|
||||
MESSAGE SENT
|
||||
</p>
|
||||
<p className="text-foreground-muted leading-relaxed">{state.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const err = state.fieldErrors ?? {};
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-5">
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
className="hidden"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contact-name"
|
||||
className="block font-display text-xs tracking-[0.3em] text-foreground-muted mb-2"
|
||||
>
|
||||
NAME
|
||||
</label>
|
||||
<input
|
||||
id="contact-name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Your name"
|
||||
className={`${inputBase} ${err.name ? "border-red-400" : "border-border focus:border-accent"}`}
|
||||
/>
|
||||
{err.name && (
|
||||
<p className="text-xs text-red-400 mt-1.5">{err.name}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contact-email"
|
||||
className="block font-display text-xs tracking-[0.3em] text-foreground-muted mb-2"
|
||||
>
|
||||
EMAIL
|
||||
</label>
|
||||
<input
|
||||
id="contact-email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
className={`${inputBase} ${err.email ? "border-red-400" : "border-border focus:border-accent"}`}
|
||||
/>
|
||||
{err.email && (
|
||||
<p className="text-xs text-red-400 mt-1.5">{err.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contact-subject"
|
||||
className="block font-display text-xs tracking-[0.3em] text-foreground-muted mb-2"
|
||||
>
|
||||
SUBJECT
|
||||
</label>
|
||||
<input
|
||||
id="contact-subject"
|
||||
name="subject"
|
||||
type="text"
|
||||
required
|
||||
placeholder="What is this about?"
|
||||
className={`${inputBase} ${err.subject ? "border-red-400" : "border-border focus:border-accent"}`}
|
||||
/>
|
||||
{err.subject && (
|
||||
<p className="text-xs text-red-400 mt-1.5">{err.subject}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contact-message"
|
||||
className="block font-display text-xs tracking-[0.3em] text-foreground-muted mb-2"
|
||||
>
|
||||
MESSAGE
|
||||
</label>
|
||||
<textarea
|
||||
id="contact-message"
|
||||
name="message"
|
||||
rows={6}
|
||||
required
|
||||
placeholder="Your message…"
|
||||
className={`${inputBase} resize-y ${err.message ? "border-red-400" : "border-border focus:border-accent"}`}
|
||||
/>
|
||||
{err.message && (
|
||||
<p className="text-xs text-red-400 mt-1.5">{err.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pt-2">
|
||||
<SubmitButton />
|
||||
{state.status === "error" && !Object.keys(err).length && (
|
||||
<p className="text-sm text-red-400">{state.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { SocialLinks } from "./SocialLinks";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border mt-24">
|
||||
<div className="max-w-7xl mx-auto px-6 lg:px-10 py-12 grid grid-cols-1 md:grid-cols-4 gap-10">
|
||||
<div className="md:col-span-2">
|
||||
<Link href="/" className="group inline-flex items-center gap-3">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt="Highland Games Studio"
|
||||
width={32}
|
||||
height={32}
|
||||
className="transition-transform duration-300 group-hover:scale-110 group-hover:-translate-y-0.5"
|
||||
/>
|
||||
<span className="font-display text-lg tracking-wider transition-colors duration-300 group-hover:text-accent">
|
||||
Highland Games Studio
|
||||
</span>
|
||||
</Link>
|
||||
<p className="mt-4 text-sm text-foreground-muted max-w-sm">
|
||||
Where summits become worlds. An indie studio crafting survival
|
||||
adventures.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-display text-sm tracking-widest text-foreground-muted mb-4">
|
||||
Explore
|
||||
</h4>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/studio"
|
||||
className="group relative inline-block text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Studio
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/games"
|
||||
className="group relative inline-block text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Games
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/devblog"
|
||||
className="group relative inline-block text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Devblog
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="group relative inline-block text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Contact
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-display text-sm tracking-widest text-foreground-muted mb-4">
|
||||
Follow
|
||||
</h4>
|
||||
<SocialLinks />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border">
|
||||
<div className="max-w-7xl mx-auto px-6 lg:px-10 py-6 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs text-foreground-muted">
|
||||
<p>© {new Date().getFullYear()} Highland Games Studio. All rights reserved.</p>
|
||||
<div className="flex items-center gap-5">
|
||||
<Link
|
||||
href="/legal/notice"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
Legal Notice
|
||||
</Link>
|
||||
<Link
|
||||
href="/legal/privacy"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
Privacy
|
||||
</Link>
|
||||
</div>
|
||||
<p className="font-display tracking-widest hidden lg:block">WHERE SUMMITS BECOME WORLDS</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
const links = [
|
||||
{ href: "/studio", label: "Studio" },
|
||||
{ href: "/games", label: "Games" },
|
||||
{ href: "/devblog", label: "Devblog" },
|
||||
{ href: "/contact", label: "Contact" },
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 20);
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = prev;
|
||||
};
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
initial={{ y: -20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled || open
|
||||
? "bg-background/80 backdrop-blur-md border-b border-border"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<nav className="max-w-7xl mx-auto flex items-center justify-between px-6 lg:px-10 h-16 lg:h-20">
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt="Highland Games Studio"
|
||||
width={36}
|
||||
height={36}
|
||||
className="transition-transform group-hover:-translate-y-0.5"
|
||||
priority
|
||||
/>
|
||||
<span className="font-display text-xl tracking-wider hidden sm:block">
|
||||
Highland Games
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<ul className="hidden md:flex items-center gap-1 sm:gap-2">
|
||||
{links.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="px-3 py-2 text-sm tracking-wide text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Mobile toggle */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={open ? "Close menu" : "Open menu"}
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-menu"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="md:hidden relative w-10 h-10 flex items-center justify-center text-foreground hover:text-accent transition-colors"
|
||||
>
|
||||
<span className="sr-only">Menu</span>
|
||||
<span className="relative w-6 h-4 flex flex-col justify-between">
|
||||
<motion.span
|
||||
animate={open ? { rotate: 45, y: 7 } : { rotate: 0, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="block h-px bg-current origin-center"
|
||||
/>
|
||||
<motion.span
|
||||
animate={open ? { opacity: 0 } : { opacity: 1 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="block h-px bg-current"
|
||||
/>
|
||||
<motion.span
|
||||
animate={open ? { rotate: -45, y: -7 } : { rotate: 0, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="block h-px bg-current origin-center"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
id="mobile-menu"
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.25, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="md:hidden border-t border-border bg-background/95 backdrop-blur-md"
|
||||
>
|
||||
<ul className="px-6 py-6 flex flex-col gap-1">
|
||||
{links.map((link, i) => (
|
||||
<motion.li
|
||||
key={link.href}
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.05 + i * 0.04, duration: 0.3 }}
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="block py-3 font-display text-2xl tracking-wider text-foreground hover:text-accent transition-colors"
|
||||
>
|
||||
{link.label.toUpperCase()}
|
||||
</Link>
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState } from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { subscribe, type SubscribeState } from "@/app/actions/subscribe";
|
||||
|
||||
const initialState: SubscribeState = { status: "idle", message: "" };
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="px-6 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground hover:scale-[1.03] transition duration-200 disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
>
|
||||
{pending ? "SENDING…" : "SUBSCRIBE"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
variant?: "default" | "compact";
|
||||
};
|
||||
|
||||
export function Newsletter({ variant = "default" }: Props) {
|
||||
const [state, formAction] = useActionState(subscribe, initialState);
|
||||
|
||||
const wrapperClass =
|
||||
variant === "compact"
|
||||
? "w-full max-w-lg"
|
||||
: "w-full max-w-lg mx-auto";
|
||||
|
||||
return (
|
||||
<form
|
||||
action={formAction}
|
||||
className={wrapperClass}
|
||||
aria-describedby="newsletter-status"
|
||||
>
|
||||
{/* honeypot for bots */}
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
className="hidden"
|
||||
aria-hidden
|
||||
/>
|
||||
<label htmlFor="newsletter-email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<input
|
||||
id="newsletter-email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="your@email.com"
|
||||
className="flex-1 min-w-0 bg-surface border border-border px-4 py-3 text-sm text-foreground placeholder:text-foreground-muted focus:outline-none focus:border-accent transition-colors"
|
||||
/>
|
||||
<SubmitButton />
|
||||
</div>
|
||||
|
||||
<p
|
||||
id="newsletter-status"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className={`text-xs tracking-widest mt-3 ${
|
||||
variant === "compact" ? "" : "text-center"
|
||||
} ${
|
||||
state.status === "success"
|
||||
? "text-accent"
|
||||
: state.status === "error"
|
||||
? "text-red-400"
|
||||
: "text-foreground-muted"
|
||||
}`}
|
||||
>
|
||||
{state.message ||
|
||||
"No spam. One devblog email per major update. Unsubscribe anytime."}
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Social = {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
const socials: Social[] = [
|
||||
{
|
||||
label: "Twitter / X",
|
||||
href: "#",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Discord",
|
||||
href: "#",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "YouTube",
|
||||
href: "#",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M23.498 6.186a3.02 3.02 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.02 3.02 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.02 3.02 0 0 0 2.122 2.136C4.495 20.455 12 20.455 12 20.455s7.505 0 9.377-.505a3.02 3.02 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Steam",
|
||||
href: "#",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.456-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export function SocialLinks() {
|
||||
return (
|
||||
<ul className="space-y-2 text-sm">
|
||||
{socials.map((s) => (
|
||||
<li key={s.label}>
|
||||
<motion.a
|
||||
href={s.href}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
animate="rest"
|
||||
className="group inline-flex items-center text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
<motion.span
|
||||
variants={{
|
||||
rest: { width: 0, opacity: 0, marginRight: 0 },
|
||||
hover: { width: 16, opacity: 1, marginRight: 10 },
|
||||
}}
|
||||
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="inline-flex items-center justify-center overflow-hidden text-accent"
|
||||
>
|
||||
<motion.span
|
||||
variants={{
|
||||
rest: { x: -8 },
|
||||
hover: { x: 0 },
|
||||
}}
|
||||
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="block w-4 h-4"
|
||||
>
|
||||
{s.icon}
|
||||
</motion.span>
|
||||
</motion.span>
|
||||
<span>{s.label}</span>
|
||||
</motion.a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ContactForm } from "../components/ContactForm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Contact",
|
||||
description: "Get in touch with Highland Games Studio.",
|
||||
};
|
||||
|
||||
const channels = [
|
||||
{
|
||||
label: "GENERAL",
|
||||
value: "hello@highlandgamesstudio.com",
|
||||
href: "mailto:hello@highlandgamesstudio.com",
|
||||
},
|
||||
{
|
||||
label: "PRESS",
|
||||
value: "press@highlandgamesstudio.com",
|
||||
href: "mailto:press@highlandgamesstudio.com",
|
||||
},
|
||||
{
|
||||
label: "BUSINESS",
|
||||
value: "business@highlandgamesstudio.com",
|
||||
href: "mailto:business@highlandgamesstudio.com",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className="pt-32 pb-24 px-6 lg:px-10">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— GET IN TOUCH
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-6xl lg:text-7xl tracking-wider leading-[0.95] mb-8">
|
||||
SAY HELLO.
|
||||
</h1>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl mb-16">
|
||||
Press inquiries, business opportunities, fan messages — all are
|
||||
welcome. Pick a channel and reach out.
|
||||
</p>
|
||||
|
||||
<div className="grid sm:grid-cols-3 gap-6 mb-24">
|
||||
{channels.map((c) => (
|
||||
<a
|
||||
key={c.label}
|
||||
href={c.href}
|
||||
className="border border-border p-8 hover:border-accent transition-colors group"
|
||||
>
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-3">
|
||||
{c.label}
|
||||
</p>
|
||||
<p className="text-xs text-foreground group-hover:text-accent transition-colors">
|
||||
{c.value}
|
||||
</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-16">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-3">
|
||||
— OR
|
||||
</p>
|
||||
<h2 className="font-display text-3xl sm:text-4xl tracking-wider mb-4">
|
||||
SEND US A MESSAGE
|
||||
</h2>
|
||||
<p className="text-foreground-muted leading-relaxed max-w-2xl mb-10">
|
||||
Prefer not to use email? Drop your message here and we'll get
|
||||
back to you.
|
||||
</p>
|
||||
<ContactForm />
|
||||
</div>
|
||||
|
||||
<div className="mt-24 border-t border-border pt-12">
|
||||
<h2 className="font-display text-2xl tracking-wider mb-4">
|
||||
PRESS KIT
|
||||
</h2>
|
||||
<p className="text-foreground-muted leading-relaxed mb-6 max-w-xl">
|
||||
Logos, screenshots and fact sheets for our games will be available
|
||||
here soon. In the meantime, drop us an email and we'll send
|
||||
assets directly.
|
||||
</p>
|
||||
<a
|
||||
href="mailto:press@highlandgamesstudio.com?subject=Press%20Kit%20Request"
|
||||
className="inline-block px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
REQUEST PRESS KIT →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { MDXRemote } from "next-mdx-remote/rsc";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { getAdjacentPosts, getAllPosts, getPost } from "@/lib/devblog";
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export function generateStaticParams() {
|
||||
return getAllPosts().map((p) => ({ slug: p.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = getPost(slug);
|
||||
if (!post) return {};
|
||||
const url = `https://highlandgamesstudio.com/devblog/${post.slug}`;
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
alternates: { canonical: url },
|
||||
openGraph: {
|
||||
type: "article",
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
url,
|
||||
publishedTime: post.date,
|
||||
authors: post.author ? [post.author] : undefined,
|
||||
tags: post.tags,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
const mdxComponents = {
|
||||
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h1
|
||||
{...props}
|
||||
className="font-display text-4xl sm:text-5xl tracking-wider mt-12 mb-6 first:mt-0"
|
||||
/>
|
||||
),
|
||||
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h2
|
||||
{...props}
|
||||
className="font-display text-3xl tracking-wider mt-12 mb-4"
|
||||
/>
|
||||
),
|
||||
h3: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h3
|
||||
{...props}
|
||||
className="font-display text-2xl tracking-wider mt-8 mb-3"
|
||||
/>
|
||||
),
|
||||
p: (props: React.HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p {...props} className="text-foreground-muted leading-relaxed mb-5" />
|
||||
),
|
||||
ul: (props: React.HTMLAttributes<HTMLUListElement>) => (
|
||||
<ul
|
||||
{...props}
|
||||
className="text-foreground-muted leading-relaxed mb-5 ml-6 list-disc space-y-2"
|
||||
/>
|
||||
),
|
||||
ol: (props: React.HTMLAttributes<HTMLOListElement>) => (
|
||||
<ol
|
||||
{...props}
|
||||
className="text-foreground-muted leading-relaxed mb-5 ml-6 list-decimal space-y-2"
|
||||
/>
|
||||
),
|
||||
a: (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<a
|
||||
{...props}
|
||||
className="text-accent hover:text-foreground underline underline-offset-4 transition-colors"
|
||||
/>
|
||||
),
|
||||
strong: (props: React.HTMLAttributes<HTMLElement>) => (
|
||||
<strong {...props} className="text-foreground font-semibold" />
|
||||
),
|
||||
blockquote: (props: React.HTMLAttributes<HTMLQuoteElement>) => (
|
||||
<blockquote
|
||||
{...props}
|
||||
className="border-l-2 border-accent pl-6 my-6 italic text-foreground"
|
||||
/>
|
||||
),
|
||||
code: (props: React.HTMLAttributes<HTMLElement>) => (
|
||||
<code
|
||||
{...props}
|
||||
className="bg-surface-elevated border border-border px-1.5 py-0.5 rounded text-sm text-accent"
|
||||
/>
|
||||
),
|
||||
hr: () => <hr className="my-12 border-border" />,
|
||||
};
|
||||
|
||||
export default async function DevblogPost({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const post = getPost(slug);
|
||||
if (!post) notFound();
|
||||
|
||||
const { newer, older } = getAdjacentPosts(slug);
|
||||
const url = `https://highlandgamesstudio.com/devblog/${post.slug}`;
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: post.title,
|
||||
description: post.excerpt,
|
||||
datePublished: post.date,
|
||||
dateModified: post.date,
|
||||
author: {
|
||||
"@type": "Organization",
|
||||
name: post.author ?? "Highland Games Studio",
|
||||
},
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "Highland Games Studio",
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: "https://highlandgamesstudio.com/image/Studio/Logo Flat.png",
|
||||
},
|
||||
},
|
||||
mainEntityOfPage: { "@type": "WebPage", "@id": url },
|
||||
url,
|
||||
keywords: post.tags?.join(", "),
|
||||
};
|
||||
|
||||
return (
|
||||
<article className="pt-32 pb-24 px-6 lg:px-10">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<Link
|
||||
href="/devblog"
|
||||
className="inline-block font-display text-xs tracking-widest text-foreground-muted hover:text-accent transition-colors mb-12"
|
||||
>
|
||||
← BACK TO DEVBLOG
|
||||
</Link>
|
||||
|
||||
<header className="mb-16 pb-8 border-b border-border">
|
||||
<time
|
||||
dateTime={post.date}
|
||||
className="font-display text-xs tracking-[0.3em] text-accent"
|
||||
>
|
||||
{formatDate(post.date).toUpperCase()}
|
||||
</time>
|
||||
<h1 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider leading-[0.95] mt-4 mb-6">
|
||||
{post.title.toUpperCase()}
|
||||
</h1>
|
||||
{post.excerpt && (
|
||||
<p className="text-lg text-foreground-muted leading-relaxed">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
{post.author && (
|
||||
<p className="text-xs tracking-widest text-foreground-muted mt-6 uppercase">
|
||||
By {post.author}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="prose-custom">
|
||||
<MDXRemote
|
||||
source={post.content}
|
||||
components={mdxComponents}
|
||||
options={{
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(newer || older) && (
|
||||
<nav
|
||||
aria-label="Post navigation"
|
||||
className="mt-20 pt-10 border-t border-border grid sm:grid-cols-2 gap-6"
|
||||
>
|
||||
{newer ? (
|
||||
<Link
|
||||
href={`/devblog/${newer.slug}`}
|
||||
className="group block border border-border p-6 hover:border-accent transition-colors"
|
||||
>
|
||||
<p className="font-display text-xs tracking-[0.3em] text-foreground-muted group-hover:text-accent transition-colors mb-2">
|
||||
← NEWER
|
||||
</p>
|
||||
<p className="font-display text-lg tracking-wider group-hover:text-accent transition-colors">
|
||||
{newer.title.toUpperCase()}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
)}
|
||||
{older ? (
|
||||
<Link
|
||||
href={`/devblog/${older.slug}`}
|
||||
className="group block border border-border p-6 hover:border-accent transition-colors sm:text-right"
|
||||
>
|
||||
<p className="font-display text-xs tracking-[0.3em] text-foreground-muted group-hover:text-accent transition-colors mb-2">
|
||||
OLDER →
|
||||
</p>
|
||||
<p className="font-display text-lg tracking-wider group-hover:text-accent transition-colors">
|
||||
{older.title.toUpperCase()}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { getAllPosts } from "@/lib/devblog";
|
||||
import { games as gameRegistry } from "@/lib/games";
|
||||
import { Newsletter } from "../components/Newsletter";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Devblog",
|
||||
description:
|
||||
"Behind-the-scenes updates, design decisions, and milestones from Highland Games Studio.",
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function buildHref(params: { game?: string; page?: number }) {
|
||||
const search = new URLSearchParams();
|
||||
if (params.game) search.set("game", params.game);
|
||||
if (params.page && params.page > 1) search.set("page", String(params.page));
|
||||
const qs = search.toString();
|
||||
return qs ? `/devblog?${qs}` : "/devblog";
|
||||
}
|
||||
|
||||
type Props = {
|
||||
searchParams: Promise<{ game?: string; page?: string }>;
|
||||
};
|
||||
|
||||
export default async function DevblogPage({ searchParams }: Props) {
|
||||
const { game, page } = await searchParams;
|
||||
const allPosts = getAllPosts();
|
||||
|
||||
const usedGameSlugs = Array.from(
|
||||
new Set(allPosts.map((p) => p.game).filter((g): g is string => Boolean(g))),
|
||||
);
|
||||
const gameOptions = usedGameSlugs
|
||||
.map((slug) => ({
|
||||
slug,
|
||||
title: gameRegistry.find((g) => g.slug === slug)?.title ?? slug,
|
||||
}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
const filtered = allPosts.filter((p) => {
|
||||
if (game && p.game !== game) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const currentPage = Math.max(1, parseInt(page ?? "1", 10) || 1);
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||
const safePage = Math.min(currentPage, totalPages);
|
||||
const visible = filtered.slice(
|
||||
(safePage - 1) * PAGE_SIZE,
|
||||
safePage * PAGE_SIZE,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pt-32 pb-24 px-6 lg:px-10">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— DEVBLOG
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-6xl lg:text-7xl tracking-wider leading-[0.95] mb-8">
|
||||
THE JOURNEY,
|
||||
<br />
|
||||
ONE STEP AT A TIME.
|
||||
</h1>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl mb-12">
|
||||
Honest updates from the studio. Design decisions, prototypes, art
|
||||
milestones everything that goes into building our worlds.
|
||||
</p>
|
||||
|
||||
{gameOptions.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2 mb-12">
|
||||
<span className="font-display text-xs tracking-[0.3em] text-foreground-muted mr-2">
|
||||
GAME
|
||||
</span>
|
||||
<Link
|
||||
href={buildHref({})}
|
||||
className={`text-xs tracking-widest uppercase border px-3 py-1.5 transition-colors ${
|
||||
!game
|
||||
? "border-accent text-accent"
|
||||
: "border-border text-foreground-muted hover:border-accent hover:text-accent"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</Link>
|
||||
{gameOptions.map((g) => (
|
||||
<Link
|
||||
key={g.slug}
|
||||
href={buildHref({ game: g.slug })}
|
||||
className={`text-xs tracking-widest uppercase border px-3 py-1.5 transition-colors ${
|
||||
game === g.slug
|
||||
? "border-accent text-accent"
|
||||
: "border-border text-foreground-muted hover:border-accent hover:text-accent"
|
||||
}`}
|
||||
>
|
||||
{g.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visible.length === 0 ? (
|
||||
<p className="text-foreground-muted">
|
||||
{game
|
||||
? "No posts match these filters yet."
|
||||
: "No posts yet. Check back soon."}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{visible.map((post) => {
|
||||
const postGame = post.game
|
||||
? gameRegistry.find((g) => g.slug === post.game)?.title ??
|
||||
post.game
|
||||
: null;
|
||||
return (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/devblog/${post.slug}`}
|
||||
className="group block py-8 border-t border-border hover:border-accent transition-colors"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-baseline sm:justify-between gap-2 mb-3">
|
||||
<h2 className="font-display text-2xl sm:text-3xl tracking-wider group-hover:text-accent transition-colors">
|
||||
{post.title.toUpperCase()}
|
||||
</h2>
|
||||
<time
|
||||
dateTime={post.date}
|
||||
className="text-xs tracking-widest text-foreground-muted shrink-0"
|
||||
>
|
||||
{formatDate(post.date)}
|
||||
</time>
|
||||
</div>
|
||||
{post.excerpt && (
|
||||
<p className="text-foreground-muted leading-relaxed max-w-2xl">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
{postGame && (
|
||||
<div className="flex flex-wrap items-center gap-2 mt-4">
|
||||
<span className="text-[0.65rem] tracking-widest uppercase border border-accent/40 text-accent px-2 py-1">
|
||||
{postGame}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="inline-block mt-4 font-display text-xs tracking-widest text-foreground-muted group-hover:text-accent transition-colors">
|
||||
READ →
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<nav
|
||||
aria-label="Pagination"
|
||||
className="flex items-center justify-between mt-12 pt-8 border-t border-border"
|
||||
>
|
||||
{safePage > 1 ? (
|
||||
<Link
|
||||
href={buildHref({ game, page: safePage - 1 })}
|
||||
className="font-display text-xs tracking-widest text-foreground-muted hover:text-accent transition-colors"
|
||||
>
|
||||
← NEWER
|
||||
</Link>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
)}
|
||||
<span className="font-display text-xs tracking-widest text-foreground-muted">
|
||||
PAGE {safePage} / {totalPages}
|
||||
</span>
|
||||
{safePage < totalPages ? (
|
||||
<Link
|
||||
href={buildHref({ game, page: safePage + 1 })}
|
||||
className="font-display text-xs tracking-widest text-foreground-muted hover:text-accent transition-colors"
|
||||
>
|
||||
OLDER →
|
||||
</Link>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<section className="mt-24 border-t border-border pt-16">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-3">
|
||||
— NEWSLETTER
|
||||
</p>
|
||||
<h2 className="font-display text-3xl sm:text-4xl tracking-wider mb-4">
|
||||
DON'T MISS A SUMMIT
|
||||
</h2>
|
||||
<p className="text-foreground-muted leading-relaxed max-w-2xl mb-6">
|
||||
Subscribe to get devblog highlights and release news in your inbox.
|
||||
</p>
|
||||
<Newsletter variant="compact" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error("[app error]", error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen flex items-center justify-center px-6 lg:px-10 overflow-hidden isolate">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-0"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 60%, rgba(196,184,150,0.08) 0%, rgba(10,10,10,0) 60%)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="animate-orb-pulse absolute left-1/2 top-1/2 z-0 w-[500px] h-[500px] sm:w-[700px] sm:h-[700px] rounded-full pointer-events-none"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(196,184,150,0.35) 0%, rgba(196,184,150,0.15) 30%, rgba(196,184,150,0) 70%)",
|
||||
filter: "blur(80px)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 max-w-2xl w-full text-center pt-24 pb-12">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt=""
|
||||
width={120}
|
||||
height={80}
|
||||
className="mx-auto mb-10 opacity-40"
|
||||
/>
|
||||
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
ERROR
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-6xl lg:text-7xl tracking-wider leading-[0.95] mb-8">
|
||||
AVALANCHE
|
||||
<br />
|
||||
ON THE PATH.
|
||||
</h1>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-xl mx-auto mb-4">
|
||||
Something went wrong on our side. The team has been notified.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="text-xs tracking-widest text-foreground-muted mb-12 uppercase">
|
||||
Ref: {error.digest}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center mt-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="px-8 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground transition-colors"
|
||||
>
|
||||
TRY AGAIN
|
||||
</button>
|
||||
<Link
|
||||
href="/"
|
||||
className="px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent transition-colors"
|
||||
>
|
||||
BACK TO BASE CAMP
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,59 @@
|
||||
import { getAllPosts } from "@/lib/devblog";
|
||||
|
||||
const SITE_URL = "https://highlandgamesstudio.com";
|
||||
const SITE_TITLE = "Highland Games Studio — Devblog";
|
||||
const SITE_DESCRIPTION =
|
||||
"Behind-the-scenes updates, design decisions, and milestones from Highland Games Studio.";
|
||||
|
||||
function escapeXml(input: string): string {
|
||||
return input
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const posts = getAllPosts();
|
||||
const lastBuildDate = new Date().toUTCString();
|
||||
const latest = posts[0];
|
||||
const pubDate = latest
|
||||
? new Date(latest.date).toUTCString()
|
||||
: lastBuildDate;
|
||||
|
||||
const items = posts
|
||||
.map((post) => {
|
||||
const url = `${SITE_URL}/devblog/${post.slug}`;
|
||||
return `
|
||||
<item>
|
||||
<title>${escapeXml(post.title)}</title>
|
||||
<link>${url}</link>
|
||||
<guid isPermaLink="true">${url}</guid>
|
||||
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
|
||||
${post.author ? `<author>${escapeXml(post.author)}</author>` : ""}
|
||||
<description>${escapeXml(post.excerpt || "")}</description>
|
||||
</item>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>${escapeXml(SITE_TITLE)}</title>
|
||||
<link>${SITE_URL}/devblog</link>
|
||||
<description>${escapeXml(SITE_DESCRIPTION)}</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
||||
<pubDate>${pubDate}</pubDate>
|
||||
<atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml" />${items}
|
||||
</channel>
|
||||
</rss>`;
|
||||
|
||||
return new Response(xml, {
|
||||
headers: {
|
||||
"Content-Type": "application/rss+xml; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { games, getGame } from "@/lib/games";
|
||||
import { getPostsByGame } from "@/lib/devblog";
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export function generateStaticParams() {
|
||||
return games.map((g) => ({ slug: g.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const game = getGame(slug);
|
||||
if (!game) return {};
|
||||
return {
|
||||
title: game.title,
|
||||
description: game.tagline,
|
||||
};
|
||||
}
|
||||
|
||||
const statusLabel: Record<string, string> = {
|
||||
"in-development": "IN DEVELOPMENT",
|
||||
released: "RELEASED",
|
||||
"coming-soon": "COMING SOON",
|
||||
};
|
||||
|
||||
export default async function GamePage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const game = getGame(slug);
|
||||
if (!game) notFound();
|
||||
|
||||
const relatedPosts = getPostsByGame(slug).slice(0, 3);
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "VideoGame",
|
||||
name: game.title,
|
||||
description: game.description,
|
||||
genre: game.genres,
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "Highland Games Studio",
|
||||
url: "https://highlandgamesstudio.com",
|
||||
},
|
||||
url: `https://highlandgamesstudio.com/games/${game.slug}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<article className="pt-24 pb-24">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
{/* HERO */}
|
||||
<section className="relative aspect-[21/9] sm:aspect-[21/8] bg-surface border-b border-border overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt=""
|
||||
width={220}
|
||||
height={148}
|
||||
className="opacity-20"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/40 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 px-6 lg:px-10 pb-10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-2">
|
||||
{statusLabel[game.status]}
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-7xl lg:text-8xl tracking-wider leading-[0.95]">
|
||||
{game.title.toUpperCase()}
|
||||
</h1>
|
||||
<p className="mt-4 text-lg sm:text-xl text-foreground-muted max-w-2xl">
|
||||
{game.tagline}
|
||||
</p>
|
||||
{game.steamUrl && (
|
||||
<a
|
||||
href={game.steamUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block mt-6 px-8 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
WISHLIST ON STEAM →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CONTENT */}
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-10 mt-16 grid lg:grid-cols-3 gap-12">
|
||||
<div className="lg:col-span-2">
|
||||
<h2 className="font-display text-3xl tracking-wider mb-6">ABOUT</h2>
|
||||
<p className="text-foreground-muted leading-relaxed text-lg">
|
||||
{game.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<aside className="space-y-8">
|
||||
<div>
|
||||
<h3 className="font-display text-xs tracking-[0.3em] text-accent mb-3">
|
||||
GENRES
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{game.genres.map((g) => (
|
||||
<span
|
||||
key={g}
|
||||
className="text-xs tracking-widest uppercase border border-border px-3 py-1.5"
|
||||
>
|
||||
{g}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{game.releaseWindow && (
|
||||
<div>
|
||||
<h3 className="font-display text-xs tracking-[0.3em] text-accent mb-3">
|
||||
RELEASE
|
||||
</h3>
|
||||
<p className="text-foreground">{game.releaseWindow}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{game.steamUrl && (
|
||||
<Link
|
||||
href={game.steamUrl}
|
||||
className="block px-6 py-3 bg-accent text-background font-display tracking-widest text-sm text-center hover:bg-foreground hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
WISHLIST ON STEAM
|
||||
</Link>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{relatedPosts.length > 0 && (
|
||||
<section className="max-w-6xl mx-auto px-6 lg:px-10 mt-24">
|
||||
<div className="flex items-baseline justify-between mb-8">
|
||||
<div>
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-2">
|
||||
— DEVBLOG
|
||||
</p>
|
||||
<h2 className="font-display text-3xl sm:text-4xl tracking-wider">
|
||||
LATEST ARTICLES
|
||||
</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/devblog"
|
||||
className="hidden sm:inline-block font-display text-xs tracking-widest text-foreground-muted hover:text-accent transition-colors"
|
||||
>
|
||||
ALL POSTS →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{relatedPosts.map((post) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/devblog/${post.slug}`}
|
||||
className="group block py-6 border-t border-border hover:border-accent transition-colors"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-baseline sm:justify-between gap-2 mb-2">
|
||||
<h3 className="font-display text-xl sm:text-2xl tracking-wider group-hover:text-accent transition-colors">
|
||||
{post.title.toUpperCase()}
|
||||
</h3>
|
||||
<time
|
||||
dateTime={post.date}
|
||||
className="text-xs tracking-widest text-foreground-muted shrink-0"
|
||||
>
|
||||
{formatDate(post.date)}
|
||||
</time>
|
||||
</div>
|
||||
{post.excerpt && (
|
||||
<p className="text-foreground-muted leading-relaxed max-w-2xl">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import type { Metadata } from "next";
|
||||
import { games } from "@/lib/games";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Games",
|
||||
description: "Games crafted by Highland Games Studio.",
|
||||
};
|
||||
|
||||
const statusLabel: Record<string, string> = {
|
||||
"in-development": "IN DEVELOPMENT",
|
||||
released: "RELEASED",
|
||||
"coming-soon": "COMING SOON",
|
||||
};
|
||||
|
||||
export default function GamesPage() {
|
||||
return (
|
||||
<div className="pt-32 pb-24 px-6 lg:px-10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— OUR WORLDS
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-6xl lg:text-7xl tracking-wider leading-[0.95] mb-16">
|
||||
GAMES
|
||||
</h1>
|
||||
|
||||
<div className="grid gap-8">
|
||||
{games.map((game) => (
|
||||
<Link
|
||||
key={game.slug}
|
||||
href={`/games/${game.slug}`}
|
||||
className="group relative aspect-[16/9] sm:aspect-[21/9] bg-surface border border-border overflow-hidden block hover:border-accent transition-colors"
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt=""
|
||||
width={140}
|
||||
height={94}
|
||||
className="opacity-15 group-hover:opacity-25 group-hover:scale-105 transition-all duration-700"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 p-8 bg-gradient-to-t from-background via-background/80 to-transparent">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-2">
|
||||
{statusLabel[game.status]}
|
||||
</p>
|
||||
<h2 className="font-display text-3xl sm:text-4xl tracking-wider mb-3">
|
||||
{game.title.toUpperCase()}
|
||||
</h2>
|
||||
<p className="text-foreground-muted max-w-2xl">
|
||||
{game.tagline}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #e8dfcc;
|
||||
--foreground-muted: #a89e8a;
|
||||
--surface: #141414;
|
||||
--surface-elevated: #1c1c1c;
|
||||
--border: #2a2a2a;
|
||||
--accent: #c4b896;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-foreground-muted: var(--foreground-muted);
|
||||
--color-surface: var(--surface);
|
||||
--color-surface-elevated: var(--surface-elevated);
|
||||
--color-border: var(--border);
|
||||
--color-accent: var(--accent);
|
||||
|
||||
--font-display: var(--font-bebas), "Bebas Neue", "Arial Narrow", sans-serif;
|
||||
--font-sans: var(--font-inter), system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--foreground-muted);
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: var(--font-display);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@keyframes orb-pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.55;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
transform: translate(-50%, -50%) scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-orb-pulse {
|
||||
animation: orb-pulse 6s ease-in-out infinite;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,90 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Bebas_Neue, Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Navbar } from "./components/Navbar";
|
||||
import { Footer } from "./components/Footer";
|
||||
|
||||
const bebas = Bebas_Neue({
|
||||
variable: "--font-bebas",
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Highland Games Studio — Where summits become worlds",
|
||||
template: "%s — Highland Games Studio",
|
||||
},
|
||||
description:
|
||||
"Indie game studio crafting survival adventures. Where summits become worlds.",
|
||||
metadataBase: new URL("https://highlandgamesstudio.com"),
|
||||
openGraph: {
|
||||
title: "Highland Games Studio",
|
||||
description: "Where summits become worlds.",
|
||||
type: "website",
|
||||
siteName: "Highland Games Studio",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Highland Games Studio",
|
||||
description: "Where summits become worlds.",
|
||||
},
|
||||
alternates: {
|
||||
types: {
|
||||
"application/rss+xml": [
|
||||
{ url: "/feed.xml", title: "Highland Games Studio — Devblog" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const organizationJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: "Highland Games Studio",
|
||||
url: "https://highlandgamesstudio.com",
|
||||
logo: "https://highlandgamesstudio.com/image/Studio/Logo Flat.png",
|
||||
description:
|
||||
"Indie game studio crafting survival adventures. Where summits become worlds.",
|
||||
slogan: "Where summits become worlds",
|
||||
foundingDate: "2026",
|
||||
email: "hello@highlandgamesstudio.com",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${bebas.variable} ${inter.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col bg-background text-foreground">
|
||||
<a
|
||||
href="#main"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-accent focus:text-background focus:font-display focus:tracking-widest focus:text-sm"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
<Navbar />
|
||||
<main id="main" className="flex-1 flex flex-col">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(organizationJsonLd),
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Legal Notice",
|
||||
description: "Legal information about Highland Games Studio.",
|
||||
robots: { index: true, follow: true },
|
||||
};
|
||||
|
||||
export default function LegalNoticePage() {
|
||||
return (
|
||||
<div className="pt-32 pb-24 px-6 lg:px-10">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— LEGAL
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-6xl tracking-wider leading-[0.95] mb-12">
|
||||
LEGAL NOTICE
|
||||
</h1>
|
||||
|
||||
<div className="space-y-10 text-foreground-muted leading-relaxed">
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
PUBLISHER
|
||||
</h2>
|
||||
<p>
|
||||
This website is published by{" "}
|
||||
<strong>Mathew Simon</strong>, operating under the project name
|
||||
{" "}<strong>Highland Games Studio</strong>. The studio is
|
||||
currently pre-incorporation; this notice will be updated when the
|
||||
company is registered.
|
||||
</p>
|
||||
<ul className="mt-4 space-y-1 text-sm">
|
||||
<li>Publication director: Mathew Simon</li>
|
||||
<li>
|
||||
Postal address: available on request at{" "}
|
||||
<a
|
||||
href="mailto:hello@highlandgamesstudio.com"
|
||||
className="text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
hello@highlandgamesstudio.com
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
Contact:{" "}
|
||||
<a
|
||||
href="mailto:hello@highlandgamesstudio.com"
|
||||
className="text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
hello@highlandgamesstudio.com
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
HOSTING
|
||||
</h2>
|
||||
<p>
|
||||
This website is hosted by <strong>Hetzner Online GmbH</strong>,
|
||||
Industriestraße 25, 91710 Gunzenhausen, Germany.{" "}
|
||||
<a
|
||||
href="https://www.hetzner.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
hetzner.com
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
INTELLECTUAL PROPERTY
|
||||
</h2>
|
||||
<p>
|
||||
All content on this website (texts, logos, images, videos, source
|
||||
code, game artwork) is the exclusive property of Highland Games
|
||||
Studio, unless otherwise stated. Any reproduction, representation,
|
||||
modification, publication or adaptation, in whole or in part, is
|
||||
prohibited without prior written authorization.
|
||||
</p>
|
||||
<p className="mt-3">
|
||||
Third-party trademarks mentioned on this site (e.g. Steam,
|
||||
Discord) remain the property of their respective owners.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
LIABILITY
|
||||
</h2>
|
||||
<p>
|
||||
Highland Games Studio strives to provide accurate information on
|
||||
this website. However, it cannot be held liable for omissions,
|
||||
inaccuracies, or any direct or indirect damage resulting from the
|
||||
use of this site or external sites linked from it.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
APPLICABLE LAW
|
||||
</h2>
|
||||
<p>
|
||||
This legal notice is governed by French law. Any dispute related
|
||||
to the use of this website falls under the exclusive jurisdiction
|
||||
of the competent French courts.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="pt-8 border-t border-border text-sm">
|
||||
See also our{" "}
|
||||
<Link
|
||||
href="/legal/privacy"
|
||||
className="text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy Policy",
|
||||
description: "How Highland Games Studio handles your personal data.",
|
||||
robots: { index: true, follow: true },
|
||||
};
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="pt-32 pb-24 px-6 lg:px-10">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— LEGAL
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-6xl tracking-wider leading-[0.95] mb-6">
|
||||
PRIVACY POLICY
|
||||
</h1>
|
||||
<p className="text-sm tracking-widest text-foreground-muted uppercase mb-12">
|
||||
Last updated: May 2026
|
||||
</p>
|
||||
|
||||
<div className="space-y-10 text-foreground-muted leading-relaxed">
|
||||
<section>
|
||||
<p>
|
||||
Highland Games Studio respects your privacy. This page explains
|
||||
what data we collect, why, and how you can exercise your rights
|
||||
under the General Data Protection Regulation (GDPR).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
DATA CONTROLLER
|
||||
</h2>
|
||||
<p>
|
||||
The data controller for this website is{" "}
|
||||
<strong>Mathew Simon</strong>, operating under the project name
|
||||
Highland Games Studio. Contact:{" "}
|
||||
<a
|
||||
href="mailto:hello@highlandgamesstudio.com"
|
||||
className="text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
hello@highlandgamesstudio.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
DATA WE COLLECT
|
||||
</h2>
|
||||
<p>
|
||||
We only collect data that is strictly necessary to operate this
|
||||
site:
|
||||
</p>
|
||||
<ul className="mt-3 space-y-2 list-disc pl-6">
|
||||
<li>
|
||||
<strong>Newsletter</strong>: if you sign up, we store your email
|
||||
address with the sole purpose of sending you devblog updates.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Contact</strong>: when you email us, we receive the
|
||||
contents of your message and your email address.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Server logs</strong>: our hosting provider may keep
|
||||
technical logs (IP address, user agent, requested URL) for
|
||||
security and debugging.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
COOKIES & TRACKING
|
||||
</h2>
|
||||
<p>
|
||||
This website does not use advertising or third-party tracking
|
||||
cookies. No analytics tool is currently installed.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
LEGAL BASIS & RETENTION
|
||||
</h2>
|
||||
<ul className="mt-3 space-y-2 list-disc pl-6">
|
||||
<li>
|
||||
Newsletter: based on your <strong>consent</strong>. Stored until
|
||||
you unsubscribe.
|
||||
</li>
|
||||
<li>
|
||||
Contact emails: based on our <strong>legitimate interest</strong>
|
||||
{" "}in answering you. Kept for up to 3 years after our last
|
||||
exchange.
|
||||
</li>
|
||||
<li>
|
||||
Server logs: kept for a maximum of 12 months by our hosting
|
||||
provider.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
YOUR RIGHTS
|
||||
</h2>
|
||||
<p>
|
||||
Under the GDPR, you can request access, rectification, deletion,
|
||||
restriction, or portability of your personal data, and object to
|
||||
its processing. To exercise these rights, email us at{" "}
|
||||
<a
|
||||
href="mailto:hello@highlandgamesstudio.com"
|
||||
className="text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
hello@highlandgamesstudio.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className="mt-3">
|
||||
You also have the right to lodge a complaint with the CNIL
|
||||
(Commission Nationale de l'Informatique et des Libertés),
|
||||
the French data protection authority.{" "}
|
||||
<a
|
||||
href="https://www.cnil.fr"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
cnil.fr
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-display text-2xl tracking-wider text-foreground mb-3">
|
||||
THIRD PARTIES
|
||||
</h2>
|
||||
<p>
|
||||
We do not sell or rent your data. The site is hosted by{" "}
|
||||
<strong>Hetzner Online GmbH</strong> (Germany), which acts as a
|
||||
data processor and is bound by appropriate agreements. Server
|
||||
data stays within the European Union.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="pt-8 border-t border-border text-sm">
|
||||
See also our{" "}
|
||||
<Link
|
||||
href="/legal/notice"
|
||||
className="text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
Legal Notice
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Lost on the climb",
|
||||
description: "This summit doesn't exist — yet.",
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="relative min-h-screen flex items-center justify-center px-6 lg:px-10 overflow-hidden isolate">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-0"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 60%, rgba(196,184,150,0.08) 0%, rgba(10,10,10,0) 60%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Glowing orb */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="animate-orb-pulse absolute left-1/2 top-1/2 z-0 w-[500px] h-[500px] sm:w-[700px] sm:h-[700px] rounded-full pointer-events-none"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(196,184,150,0.35) 0%, rgba(196,184,150,0.15) 30%, rgba(196,184,150,0) 70%)",
|
||||
filter: "blur(80px)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 max-w-2xl w-full text-center pt-24 pb-12">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt=""
|
||||
width={120}
|
||||
height={80}
|
||||
className="mx-auto mb-10 opacity-40"
|
||||
/>
|
||||
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
404
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-6xl lg:text-7xl tracking-wider leading-[0.95] mb-8">
|
||||
LOST ON
|
||||
<br />
|
||||
THE CLIMB.
|
||||
</h1>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-xl mx-auto mb-12">
|
||||
This summit doesn't exist yet. The page you're looking for
|
||||
may have drifted off the map.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
href="/"
|
||||
className="px-8 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground transition-colors"
|
||||
>
|
||||
BACK TO BASE CAMP
|
||||
</Link>
|
||||
<Link
|
||||
href="/games"
|
||||
className="px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent transition-colors"
|
||||
>
|
||||
EXPLORE OUR GAMES
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
export const runtime = "edge";
|
||||
export const alt = "Highland Games Studio — Where summits become worlds";
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = "image/png";
|
||||
|
||||
export default async function OpengraphImage() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 60%, rgba(196,184,150,0.12) 0%, #0a0a0a 60%)",
|
||||
color: "#e8dfcc",
|
||||
fontFamily: "sans-serif",
|
||||
padding: 80,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
letterSpacing: 12,
|
||||
color: "#c4b896",
|
||||
marginBottom: 32,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
— HIGHLAND GAMES STUDIO
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 120,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 4,
|
||||
lineHeight: 1,
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<span>WHERE SUMMITS</span>
|
||||
<span style={{ color: "#c4b896" }}>BECOME WORLDS.</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 48,
|
||||
fontSize: 28,
|
||||
color: "#a89e8a",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
Indie survival adventures.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{ ...size }
|
||||
);
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
{/* HERO */}
|
||||
<section className="relative isolate min-h-screen flex items-center justify-center overflow-hidden">
|
||||
{/* Background video */}
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="auto"
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-0 w-full h-full object-cover"
|
||||
>
|
||||
<source src="/Video/Header_Video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
{/* Dark overlay for text readability */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-10 bg-background/60"
|
||||
/>
|
||||
{/* Vignette / radial fade to deepen the edges */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 50%, rgba(10,10,10,0) 30%, rgba(10,10,10,0.85) 100%)",
|
||||
}}
|
||||
/>
|
||||
{/* Grain / noise overlay via CSS gradient */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-10 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"repeating-linear-gradient(0deg, #fff 0, #fff 1px, transparent 1px, transparent 3px)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-20 max-w-5xl w-full px-6 lg:px-10 flex flex-col items-center text-center pt-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.92, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
transition={{ duration: 1.1, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="mb-10"
|
||||
>
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt="Highland Games Studio"
|
||||
width={180}
|
||||
height={120}
|
||||
priority
|
||||
className="drop-shadow-[0_0_40px_rgba(196,184,150,0.15)]"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.9, delay: 0.3, ease: "easeOut" }}
|
||||
className="font-display text-6xl sm:text-7xl lg:text-8xl tracking-wider leading-[0.95] text-balance"
|
||||
>
|
||||
HIGHLAND GAMES
|
||||
<br />
|
||||
<span className="text-accent">STUDIO</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.9, delay: 0.6, ease: "easeOut" }}
|
||||
className="mt-8 text-lg sm:text-xl text-foreground-muted tracking-wide max-w-xl text-balance"
|
||||
>
|
||||
Where summits become worlds.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.9, delay: 0.9, ease: "easeOut" }}
|
||||
className="mt-12 flex flex-col sm:flex-row gap-4"
|
||||
>
|
||||
<Link
|
||||
href="/games"
|
||||
className="px-8 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
EXPLORE OUR GAMES
|
||||
</Link>
|
||||
<Link
|
||||
href="/studio"
|
||||
className="px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
MEET THE STUDIO
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator - positioned relative to the section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.5, duration: 1 }}
|
||||
className="absolute z-20 bottom-10 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 text-foreground-muted text-xs tracking-widest"
|
||||
>
|
||||
<span className="font-display">SCROLL</span>
|
||||
<motion.div
|
||||
animate={{ y: [0, 8, 0] }}
|
||||
transition={{ duration: 1.8, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="w-px h-8 bg-foreground-muted"
|
||||
/>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* STUDIO INTRO */}
|
||||
<section className="py-24 px-6 lg:px-10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— THE STUDIO
|
||||
</p>
|
||||
<h2 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider leading-tight mb-8">
|
||||
Worlds beyond
|
||||
<br />
|
||||
the summit.
|
||||
</h2>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl">
|
||||
We build worlds where every horizon hides a story. Hand-crafted
|
||||
games made to be lived in, not just played through. Each one a
|
||||
different summit, a different climb.
|
||||
</p>
|
||||
<Link
|
||||
href="/studio"
|
||||
className="inline-block mt-8 font-display text-sm tracking-widest text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
LEARN MORE →
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FEATURED GAME */}
|
||||
<section className="py-24 px-6 lg:px-10 border-t border-border">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— IN DEVELOPMENT
|
||||
</p>
|
||||
<h2 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider mb-12">
|
||||
OUR FIRST WORLD
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.9, delay: 0.1 }}
|
||||
>
|
||||
<Link
|
||||
href="/games/project-one"
|
||||
className="group relative aspect-[16/9] bg-surface border border-border overflow-hidden block hover:border-accent transition-colors duration-500"
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt="Game placeholder"
|
||||
width={120}
|
||||
height={80}
|
||||
className="opacity-20 group-hover:opacity-30 group-hover:scale-105 transition-all duration-700 ease-out"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 bg-accent/0 group-hover:bg-accent/[0.03] transition-colors duration-500"
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 right-0 p-8 bg-gradient-to-t from-background via-background/70 to-transparent">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-2">
|
||||
SURVIVAL · CRAFTING · EXPLORATION
|
||||
</p>
|
||||
<h3 className="font-display text-3xl sm:text-4xl tracking-wider mb-4 group-hover:text-accent transition-colors duration-300">
|
||||
PROJECT ONE
|
||||
</h3>
|
||||
<p className="text-foreground-muted max-w-xl mb-6">
|
||||
Inspired by Raft and Aloft. Build, explore, and survive across
|
||||
shifting horizons. A new survival adventure, currently in early
|
||||
development.
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-2 font-display text-sm tracking-widest text-foreground group-hover:text-accent transition-colors">
|
||||
LEARN MORE
|
||||
<span className="inline-block transition-transform duration-300 group-hover:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* DEVBLOG TEASER */}
|
||||
<section className="py-24 px-6 lg:px-10 border-t border-border">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— DEVBLOG
|
||||
</p>
|
||||
<h2 className="font-display text-4xl sm:text-5xl tracking-wider mb-6">
|
||||
FOLLOW THE JOURNEY
|
||||
</h2>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl mx-auto mb-8">
|
||||
Behind-the-scenes updates, design decisions, and milestones from
|
||||
the studio. Every step of the climb, documented.
|
||||
</p>
|
||||
<Link
|
||||
href="/devblog"
|
||||
className="inline-block px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
READ THE DEVBLOG
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const SITE_URL = "https://highlandgamesstudio.com";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
],
|
||||
sitemap: `${SITE_URL}/sitemap.xml`,
|
||||
host: SITE_URL,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { games } from "@/lib/games";
|
||||
import { getAllPosts } from "@/lib/devblog";
|
||||
|
||||
const SITE_URL = "https://highlandgamesstudio.com";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const now = new Date();
|
||||
|
||||
const staticRoutes: MetadataRoute.Sitemap = [
|
||||
{ url: `${SITE_URL}/`, lastModified: now, changeFrequency: "monthly", priority: 1 },
|
||||
{ url: `${SITE_URL}/studio`, lastModified: now, changeFrequency: "monthly", priority: 0.8 },
|
||||
{ url: `${SITE_URL}/games`, lastModified: now, changeFrequency: "weekly", priority: 0.9 },
|
||||
{ url: `${SITE_URL}/devblog`, lastModified: now, changeFrequency: "weekly", priority: 0.8 },
|
||||
{ url: `${SITE_URL}/contact`, lastModified: now, changeFrequency: "yearly", priority: 0.5 },
|
||||
{ url: `${SITE_URL}/legal/notice`, lastModified: now, changeFrequency: "yearly", priority: 0.2 },
|
||||
{ url: `${SITE_URL}/legal/privacy`, lastModified: now, changeFrequency: "yearly", priority: 0.2 },
|
||||
];
|
||||
|
||||
const gameRoutes: MetadataRoute.Sitemap = games.map((g) => ({
|
||||
url: `${SITE_URL}/games/${g.slug}`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
const postRoutes: MetadataRoute.Sitemap = getAllPosts().map((p) => ({
|
||||
url: `${SITE_URL}/devblog/${p.slug}`,
|
||||
lastModified: new Date(p.date),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
return [...staticRoutes, ...gameRoutes, ...postRoutes];
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import Image from "next/image";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Studio",
|
||||
description:
|
||||
"Highland Games Studio is a two-person indie team crafting survival adventures.",
|
||||
};
|
||||
|
||||
const team = [
|
||||
{
|
||||
name: "Founder One",
|
||||
role: "Co-founder · Programming · Design",
|
||||
bio: "Builds the systems, gameplay loops and tools that bring our worlds to life.",
|
||||
},
|
||||
{
|
||||
name: "Founder Two",
|
||||
role: "Co-founder · Art · Design",
|
||||
bio: "Shapes the visual identity, environments and feel of every Highland Games world.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function StudioPage() {
|
||||
return (
|
||||
<div className="pt-32 pb-24 px-6 lg:px-10">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— THE STUDIO
|
||||
</p>
|
||||
<h1 className="font-display text-5xl sm:text-6xl lg:text-7xl tracking-wider leading-[0.95] mb-8">
|
||||
A SMALL TEAM,
|
||||
<br />
|
||||
BIG HORIZONS.
|
||||
</h1>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl">
|
||||
Highland Games Studio is an independent game studio of two friends
|
||||
building survival worlds we'd want to lose ourselves in. We
|
||||
believe small teams can craft big experiences, where every detail is
|
||||
made by someone who cares.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* MISSION */}
|
||||
<section className="max-w-4xl mx-auto mt-24 grid md:grid-cols-3 gap-8">
|
||||
<div className="border border-border p-8">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-4">
|
||||
01
|
||||
</p>
|
||||
<h3 className="font-display text-2xl tracking-wider mb-3">
|
||||
CRAFT
|
||||
</h3>
|
||||
<p className="text-sm text-foreground-muted leading-relaxed">
|
||||
Every system, asset and line of dialogue is touched by hand. No
|
||||
shortcuts on what matters.
|
||||
</p>
|
||||
</div>
|
||||
<div className="border border-border p-8">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-4">
|
||||
02
|
||||
</p>
|
||||
<h3 className="font-display text-2xl tracking-wider mb-3">
|
||||
SURVIVE
|
||||
</h3>
|
||||
<p className="text-sm text-foreground-muted leading-relaxed">
|
||||
We love the genre. The tension between scarcity and discovery is at
|
||||
the core of every game we make.
|
||||
</p>
|
||||
</div>
|
||||
<div className="border border-border p-8">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-4">
|
||||
03
|
||||
</p>
|
||||
<h3 className="font-display text-2xl tracking-wider mb-3">
|
||||
WONDER
|
||||
</h3>
|
||||
<p className="text-sm text-foreground-muted leading-relaxed">
|
||||
A summit isn't the end of the climb — it's the start of a
|
||||
new world. We design for that feeling.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* TEAM */}
|
||||
<section className="max-w-4xl mx-auto mt-24">
|
||||
<h2 className="font-display text-3xl sm:text-4xl tracking-wider mb-12">
|
||||
THE CLIMBERS
|
||||
</h2>
|
||||
<div className="grid sm:grid-cols-2 gap-8">
|
||||
{team.map((m) => (
|
||||
<div key={m.name} className="border border-border p-8 bg-surface">
|
||||
<div className="w-20 h-20 bg-surface-elevated border border-border flex items-center justify-center mb-6">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt=""
|
||||
width={40}
|
||||
height={28}
|
||||
className="opacity-40"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="font-display text-2xl tracking-wider mb-1">
|
||||
{m.name}
|
||||
</h3>
|
||||
<p className="text-xs tracking-widest text-accent mb-4 uppercase">
|
||||
{m.role}
|
||||
</p>
|
||||
<p className="text-sm text-foreground-muted leading-relaxed">
|
||||
{m.bio}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function Template({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user