Initial commit
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
(function () {
|
||||
const updateHeader = () => {
|
||||
const header = document.querySelector("[data-site-header]");
|
||||
if (!header) return;
|
||||
const compact = window.scrollY > 24;
|
||||
header.classList.toggle("is-compact", compact);
|
||||
};
|
||||
updateHeader();
|
||||
window.addEventListener("scroll", updateHeader, { passive: true });
|
||||
|
||||
function closeOverlay() {
|
||||
const root = document.getElementById("overlay-root");
|
||||
if (root) root.innerHTML = "";
|
||||
document.documentElement.classList.remove("overflow-hidden");
|
||||
}
|
||||
|
||||
document.addEventListener("htmx:afterSwap", function (event) {
|
||||
if (event.detail.target && event.detail.target.id === "overlay-root") {
|
||||
const overlay = event.detail.target.querySelector("[data-overlay]");
|
||||
if (!overlay) return;
|
||||
document.documentElement.classList.add("overflow-hidden");
|
||||
const close = overlay.querySelector("[data-overlay-close]");
|
||||
if (close) close.focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("htmx:afterSettle", updateHeader);
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (event.target.matches("[data-overlay], [data-overlay-close]")) {
|
||||
closeOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape") {
|
||||
closeOverlay();
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 1100">
|
||||
<rect width="900" height="1100" fill="#e5e1d8"/>
|
||||
<circle cx="450" cy="350" r="160" fill="#a7a197"/>
|
||||
<path d="M180 980c35-250 190-380 270-380s235 130 270 380z" fill="#2b2b2b"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 253 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1000">
|
||||
<rect width="1600" height="1000" fill="#d8d5cf"/>
|
||||
<path d="M0 760 420 430l290 210 290-360 600 500v220H0z" fill="#9d9a92"/>
|
||||
<path d="M220 710h1160v95H220z" fill="#1f2933" opacity=".85"/>
|
||||
<path d="M290 295h390v360H290zM760 210h520v445H760z" fill="#f4f0e8" opacity=".78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 350 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 900">
|
||||
<rect width="900" height="900" fill="#d7d1c5"/>
|
||||
<path d="M120 650h660v110H120z" fill="#242424"/>
|
||||
<path d="M190 230h210v420H190zM460 160h250v490H460z" fill="#f7f2e8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 900">
|
||||
<rect width="900" height="900" fill="#c7ced0"/>
|
||||
<path d="M130 200h640v500H130z" fill="#f4f4ef"/>
|
||||
<path d="M220 300h170v300H220zM500 300h170v300H500z" fill="#6d7476"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 900">
|
||||
<rect width="900" height="900" fill="#ddd9d0"/>
|
||||
<path d="M170 180h560v560H170z" fill="#f8f7f2"/>
|
||||
<circle cx="450" cy="460" r="165" fill="#a1907c"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 224 B |
@@ -0,0 +1,95 @@
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
[data-site-header].is-compact {
|
||||
padding-top: 0.85rem;
|
||||
padding-bottom: 0.85rem;
|
||||
background: rgba(250, 250, 250, 0.95);
|
||||
color: #0a0a0a;
|
||||
box-shadow: 0 1px 0 rgba(10, 10, 10, 0.08);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
[data-site-header].is-compact [data-header-brand] {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
@keyframes page-fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes page-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes panel-fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes panel-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#admin-panel {
|
||||
view-transition-name: admin-panel;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: 160ms ease both page-fade-out;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: 220ms ease both page-fade-in;
|
||||
}
|
||||
|
||||
::view-transition-old(admin-panel) {
|
||||
animation: 140ms ease both panel-fade-out;
|
||||
}
|
||||
|
||||
::view-transition-new(admin-panel) {
|
||||
animation: 180ms ease both panel-fade-in;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root),
|
||||
::view-transition-old(admin-panel),
|
||||
::view-transition-new(admin-panel) {
|
||||
animation-duration: 1ms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{template "head" .}}
|
||||
{{template "site_header" .}}
|
||||
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
|
||||
<section class="grid gap-10 md:grid-cols-[0.9fr_1.1fr] md:items-start">
|
||||
<div class="aspect-[4/5] overflow-hidden bg-neutral-200">
|
||||
<img src="{{.Content.AboutImage}}" alt="{{.Content.AboutName}}" class="h-full w-full object-cover">
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-4 text-sm uppercase tracking-[0.2em] text-neutral-500">{{.Content.AboutRole}}</p>
|
||||
<h1 class="text-5xl font-semibold md:text-7xl">{{.Content.AboutName}}</h1>
|
||||
<p class="mt-8 max-w-2xl text-xl leading-relaxed text-neutral-600">{{.Content.AboutBio}}</p>
|
||||
<div class="mt-8 grid gap-2 text-sm text-neutral-600">
|
||||
<p>{{.Content.Email}}</p>
|
||||
<p>{{.Content.Phone}}</p>
|
||||
<p>{{.Content.Location}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-20 max-w-2xl">
|
||||
<h2 class="mb-6 text-3xl font-semibold">Contact</h2>
|
||||
<form hx-post="/contact" hx-target="#contact-result" hx-swap="innerHTML" class="grid gap-4">
|
||||
<input name="name" required placeholder="Name" class="border border-neutral-300 bg-white px-4 py-3 outline-none focus:border-neutral-950">
|
||||
<input name="email" type="email" required placeholder="Email" class="border border-neutral-300 bg-white px-4 py-3 outline-none focus:border-neutral-950">
|
||||
<textarea name="message" required rows="6" placeholder="Project request" class="border border-neutral-300 bg-white px-4 py-3 outline-none focus:border-neutral-950"></textarea>
|
||||
<button class="bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700">Submit request</button>
|
||||
</form>
|
||||
<div id="contact-result" class="mt-4"></div>
|
||||
</section>
|
||||
</main>
|
||||
{{template "footer" .}}
|
||||
<div id="overlay-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
{{define "admin_shell_start"}}
|
||||
{{template "head" .}}
|
||||
<header class="sticky top-0 z-30 border-b border-neutral-200 bg-white/95 backdrop-blur">
|
||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-5 py-4 md:px-8">
|
||||
<a href="/admin/main" class="text-xl font-semibold">Archi Folio Admin</a>
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<a href="/" class="text-neutral-500 hover:text-neutral-950">View site</a>
|
||||
<form method="post" action="/admin/logout"><button class="text-neutral-500 hover:text-neutral-950">Log out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admin-tabs" class="border-t border-neutral-200">
|
||||
{{template "admin_tabs_inner" .}}
|
||||
</div>
|
||||
</header>
|
||||
<main class="mx-auto grid max-w-7xl gap-6 px-5 py-8 md:px-8">
|
||||
<div id="admin-flash">
|
||||
{{template "admin_flash_inner" .}}
|
||||
</div>
|
||||
<section id="admin-panel">
|
||||
{{end}}
|
||||
|
||||
{{define "admin_shell_end"}}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_tabs_inner"}}
|
||||
<nav class="mx-auto flex max-w-7xl gap-2 overflow-x-auto px-5 py-3 text-sm md:px-8" aria-label="Admin sections">
|
||||
<a href="/admin/main" hx-get="/admin/main" hx-target="#admin-panel" hx-push-url="true" hx-swap="innerHTML transition:true" class="whitespace-nowrap px-3 py-2 {{if eq .AdminTab "main"}}bg-neutral-950 text-white{{else}}text-neutral-500 hover:bg-neutral-100 hover:text-neutral-950{{end}}">Main</a>
|
||||
<a href="/admin/projects" hx-get="/admin/projects" hx-target="#admin-panel" hx-push-url="true" hx-swap="innerHTML transition:true" class="whitespace-nowrap px-3 py-2 {{if eq .AdminTab "projects"}}bg-neutral-950 text-white{{else}}text-neutral-500 hover:bg-neutral-100 hover:text-neutral-950{{end}}">Projects</a>
|
||||
<a href="/admin/contact-details" hx-get="/admin/contact-details" hx-target="#admin-panel" hx-push-url="true" hx-swap="innerHTML transition:true" class="whitespace-nowrap px-3 py-2 {{if eq .AdminTab "contact-details"}}bg-neutral-950 text-white{{else}}text-neutral-500 hover:bg-neutral-100 hover:text-neutral-950{{end}}">Contact Details</a>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_tabs_oob"}}
|
||||
<div id="admin-tabs" class="border-t border-neutral-200" hx-swap-oob="true">
|
||||
{{template "admin_tabs_inner" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_flash_inner"}}
|
||||
{{if .Success}}<p class="border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">{{.Success}}</p>{{end}}
|
||||
{{if .Error}}<p class="border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-900">{{.Error}}</p>{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_flash_oob"}}
|
||||
<div id="admin-flash" hx-swap-oob="true">
|
||||
{{template "admin_flash_inner" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,42 @@
|
||||
{{define "admin_contact_details.html"}}
|
||||
{{template "admin_shell_start" .}}
|
||||
{{template "admin_contact_details_panel" .}}
|
||||
{{template "admin_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_contact_details_partial.html"}}
|
||||
{{template "admin_tabs_oob" .}}
|
||||
{{template "admin_flash_oob" .}}
|
||||
{{template "admin_contact_details_panel" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_contact_details_panel"}}
|
||||
<section class="grid gap-6">
|
||||
<section class="bg-white p-6 shadow-sm">
|
||||
<h1 class="mb-6 text-2xl font-semibold">Contact Details</h1>
|
||||
<form method="post" action="/admin/contact-details" class="grid gap-4 md:grid-cols-3">
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">Email</span><input name="email" value="{{.Content.Email}}" class="w-full border px-3 py-2"></label>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">Phone</span><input name="phone" value="{{.Content.Phone}}" class="w-full border px-3 py-2"></label>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">Location</span><input name="location" value="{{.Content.Location}}" class="w-full border px-3 py-2"></label>
|
||||
<button class="w-fit bg-neutral-950 px-5 py-3 text-sm uppercase tracking-[0.18em] text-white md:col-span-3">Save contact details</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="bg-white p-6 shadow-sm">
|
||||
<h2 class="mb-6 text-2xl font-semibold">Contact Requests</h2>
|
||||
<div class="grid gap-4">
|
||||
{{range .Contacts}}
|
||||
<article class="border border-neutral-200 p-4">
|
||||
<div class="mb-2 flex flex-col gap-1 text-sm md:flex-row md:items-center md:justify-between">
|
||||
<p class="font-medium">{{.Name}} · {{.Email}}</p>
|
||||
<p class="text-neutral-500">{{.CreatedAt.Format "2006-01-02 15:04"}}</p>
|
||||
</div>
|
||||
<p class="text-neutral-700">{{.Message}}</p>
|
||||
</article>
|
||||
{{else}}
|
||||
<p class="text-neutral-500">No contact requests yet.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{template "head" .}}
|
||||
<main class="flex min-h-screen items-center justify-center bg-neutral-950 px-5 text-neutral-950">
|
||||
<form method="post" action="/admin/login" class="w-full max-w-sm bg-white p-8 shadow-2xl">
|
||||
<h1 class="mb-6 text-3xl font-semibold">Admin</h1>
|
||||
{{if .Error}}<p class="mb-4 border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-900">{{.Error}}</p>{{end}}
|
||||
<label class="mb-4 block text-sm">
|
||||
<span class="mb-2 block text-neutral-500">Username</span>
|
||||
<input name="username" required autofocus class="w-full border border-neutral-300 px-4 py-3 outline-none focus:border-neutral-950">
|
||||
</label>
|
||||
<label class="mb-6 block text-sm">
|
||||
<span class="mb-2 block text-neutral-500">Password</span>
|
||||
<input name="password" type="password" required class="w-full border border-neutral-300 px-4 py-3 outline-none focus:border-neutral-950">
|
||||
</label>
|
||||
<button class="w-full bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700">Log in</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
{{define "admin_main.html"}}
|
||||
{{template "admin_shell_start" .}}
|
||||
{{template "admin_main_panel" .}}
|
||||
{{template "admin_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_main_partial.html"}}
|
||||
{{template "admin_tabs_oob" .}}
|
||||
{{template "admin_flash_oob" .}}
|
||||
{{template "admin_main_panel" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_main_panel"}}
|
||||
<section class="bg-white p-6 shadow-sm">
|
||||
<h1 class="mb-6 text-2xl font-semibold">Main Content</h1>
|
||||
<form method="post" action="/admin/content" enctype="multipart/form-data" class="grid gap-5">
|
||||
<input type="hidden" name="hero_image_current" value="{{.Content.HeroImage}}">
|
||||
<input type="hidden" name="about_image_current" value="{{.Content.AboutImage}}">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">Hero title</span><input name="hero_title" value="{{.Content.HeroTitle}}" class="w-full border px-3 py-2"></label>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">Hero subtitle</span><input name="hero_subtitle" value="{{.Content.HeroSubtitle}}" class="w-full border px-3 py-2"></label>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">Intro title</span><input name="intro_title" value="{{.Content.IntroTitle}}" class="w-full border px-3 py-2"></label>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">About name</span><input name="about_name" value="{{.Content.AboutName}}" class="w-full border px-3 py-2"></label>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">About role</span><input name="about_role" value="{{.Content.AboutRole}}" class="w-full border px-3 py-2"></label>
|
||||
</div>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">Intro text</span><textarea name="intro_text" rows="3" class="w-full border px-3 py-2">{{.Content.IntroText}}</textarea></label>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">About bio</span><textarea name="about_bio" rows="4" class="w-full border px-3 py-2">{{.Content.AboutBio}}</textarea></label>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">Hero image</span><input name="hero_image" type="file" accept="image/png,image/jpeg,image/webp,image/gif" class="w-full border px-3 py-2"></label>
|
||||
<label class="block text-sm"><span class="mb-2 block text-neutral-500">About image</span><input name="about_image" type="file" accept="image/png,image/jpeg,image/webp,image/gif" class="w-full border px-3 py-2"></label>
|
||||
</div>
|
||||
<button class="w-fit bg-neutral-950 px-5 py-3 text-sm uppercase tracking-[0.18em] text-white">Save content</button>
|
||||
</form>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,73 @@
|
||||
{{define "admin_projects.html"}}
|
||||
{{template "admin_shell_start" .}}
|
||||
{{template "admin_projects_panel" .}}
|
||||
{{template "admin_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_projects_partial.html"}}
|
||||
{{template "admin_tabs_oob" .}}
|
||||
{{template "admin_flash_oob" .}}
|
||||
{{template "admin_projects_panel" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_projects_panel"}}
|
||||
<section class="grid gap-6">
|
||||
<section class="bg-white p-6 shadow-sm">
|
||||
<h1 class="mb-6 text-2xl font-semibold">Add Project</h1>
|
||||
<form method="post" action="/admin/projects" enctype="multipart/form-data" class="grid gap-4 md:grid-cols-2">
|
||||
<input name="title" required placeholder="Title" class="border px-3 py-2">
|
||||
<input name="slug" placeholder="Slug, optional" class="border px-3 py-2">
|
||||
<input name="location" required placeholder="Location" class="border px-3 py-2">
|
||||
<input name="year" required placeholder="Year" class="border px-3 py-2">
|
||||
<input name="category" required placeholder="Category" class="border px-3 py-2">
|
||||
<label class="flex items-center gap-2 border px-3 py-2 text-sm"><input name="featured" type="checkbox"> Featured</label>
|
||||
<textarea name="description" required rows="4" placeholder="Description" class="border px-3 py-2 md:col-span-2"></textarea>
|
||||
<input name="cover_image" type="file" accept="image/png,image/jpeg,image/webp,image/gif" class="border px-3 py-2 md:col-span-2">
|
||||
<button class="w-fit bg-neutral-950 px-5 py-3 text-sm uppercase tracking-[0.18em] text-white md:col-span-2">Create project</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-6">
|
||||
<h2 class="text-2xl font-semibold">Projects</h2>
|
||||
{{range .Projects}}
|
||||
<article class="bg-white p-6 shadow-sm">
|
||||
<form method="post" action="/admin/projects/{{.ID}}" enctype="multipart/form-data" class="grid gap-4 md:grid-cols-2">
|
||||
<input type="hidden" name="cover_image_current" value="{{.CoverImage}}">
|
||||
<input name="title" value="{{.Title}}" class="border px-3 py-2">
|
||||
<input name="slug" value="{{.Slug}}" class="border px-3 py-2">
|
||||
<input name="location" value="{{.Location}}" class="border px-3 py-2">
|
||||
<input name="year" value="{{.Year}}" class="border px-3 py-2">
|
||||
<input name="category" value="{{.Category}}" class="border px-3 py-2">
|
||||
<label class="flex items-center gap-2 border px-3 py-2 text-sm"><input name="featured" type="checkbox" {{if .Featured}}checked{{end}}> Featured</label>
|
||||
<textarea name="description" rows="4" class="border px-3 py-2 md:col-span-2">{{.Description}}</textarea>
|
||||
<div class="grid gap-3 md:col-span-2 md:grid-cols-[120px_1fr] md:items-center">
|
||||
<img src="{{.CoverImage}}" alt="" class="h-24 w-24 object-cover">
|
||||
<input name="cover_image" type="file" accept="image/png,image/jpeg,image/webp,image/gif" class="border px-3 py-2">
|
||||
</div>
|
||||
<button class="w-fit bg-neutral-950 px-5 py-3 text-sm uppercase tracking-[0.18em] text-white">Save</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/projects/{{.ID}}/delete" class="mt-3">
|
||||
<button class="text-sm text-red-700">Delete project</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 border-t pt-5">
|
||||
<h3 class="mb-3 font-semibold">Gallery</h3>
|
||||
<div class="mb-4 grid gap-3 sm:grid-cols-3 md:grid-cols-5">
|
||||
{{range .Images}}
|
||||
<div>
|
||||
<img src="{{.Path}}" alt="{{.Caption}}" class="aspect-square w-full object-cover">
|
||||
<form method="post" action="/admin/project-images/{{.ID}}/delete" class="mt-1"><button class="text-xs text-red-700">Remove</button></form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<form method="post" action="/admin/projects/{{.ID}}/images" enctype="multipart/form-data" class="grid gap-3 md:grid-cols-[1fr_1fr_auto]">
|
||||
<input name="caption" placeholder="Caption" class="border px-3 py-2">
|
||||
<input name="image" type="file" accept="image/png,image/jpeg,image/webp,image/gif" required class="border px-3 py-2">
|
||||
<button class="bg-neutral-950 px-5 py-2 text-sm uppercase tracking-[0.18em] text-white">Add image</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
</section>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,35 @@
|
||||
{{define "head"}}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{if .Title}}{{.Title}} | {{end}}Archi Folio</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<script defer src="/static/app.js"></script>
|
||||
</head>
|
||||
<body class="bg-neutral-50 text-neutral-950 antialiased">
|
||||
{{end}}
|
||||
|
||||
{{define "site_header"}}
|
||||
<header data-site-header class="fixed inset-x-0 top-0 z-40 transition-all duration-300 {{if eq .Active "home"}}py-7 text-white{{else}}bg-neutral-50/95 py-4 shadow-sm backdrop-blur text-neutral-950{{end}}">
|
||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-5 md:px-8">
|
||||
<a href="/" class="text-xl font-semibold tracking-normal md:text-3xl" data-header-brand hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Archi Folio</a>
|
||||
<nav class="flex items-center gap-4 text-sm uppercase tracking-[0.18em] md:gap-8" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
|
||||
<a class="hover:opacity-60 {{if eq .Active "projects"}}font-semibold{{end}}" href="/projects">Projects</a>
|
||||
<a class="hover:opacity-60 {{if eq .Active "about"}}font-semibold{{end}}" href="/about">About</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
{{end}}
|
||||
|
||||
{{define "footer"}}
|
||||
<footer class="border-t border-neutral-200 px-5 py-10 text-sm text-neutral-500 md:px-8">
|
||||
<div class="mx-auto flex max-w-7xl flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<p>© 2026 Archi Folio</p>
|
||||
<p>{{.Content.Email}} · {{.Content.Location}}</p>
|
||||
</div>
|
||||
</footer>
|
||||
{{end}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{if .Success}}<p class="border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">{{.Success}}</p>{{end}}
|
||||
{{if .Error}}<p class="border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-900">{{.Error}}</p>{{end}}
|
||||
@@ -0,0 +1,44 @@
|
||||
{{template "head" .}}
|
||||
{{template "site_header" .}}
|
||||
<main>
|
||||
<section class="relative min-h-[92vh] overflow-hidden bg-neutral-950">
|
||||
<img src="{{.Content.HeroImage}}" alt="" class="absolute inset-0 h-full w-full object-cover opacity-90">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/35 via-black/5 to-black/55"></div>
|
||||
<div class="relative mx-auto flex min-h-[92vh] max-w-7xl flex-col justify-end px-5 pb-16 text-white md:px-8 md:pb-20">
|
||||
<p class="mb-4 max-w-xl text-sm uppercase tracking-[0.22em] text-white/75">{{.Content.HeroSubtitle}}</p>
|
||||
<h1 class="max-w-5xl text-5xl font-semibold leading-[0.95] md:text-8xl">{{.Content.HeroTitle}}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mx-auto grid max-w-7xl gap-8 px-5 py-20 md:grid-cols-[0.8fr_1.2fr] md:px-8 md:py-28">
|
||||
<h2 class="text-3xl font-semibold md:text-5xl">{{.Content.IntroTitle}}</h2>
|
||||
<p class="max-w-2xl text-xl leading-relaxed text-neutral-600">{{.Content.IntroText}}</p>
|
||||
</section>
|
||||
|
||||
<section class="px-5 pb-24 md:px-8" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
|
||||
<div class="mx-auto mb-8 flex max-w-7xl items-end justify-between">
|
||||
<h2 class="text-2xl font-semibold md:text-4xl">Featured Projects</h2>
|
||||
<a href="/projects" class="text-sm uppercase tracking-[0.18em] text-neutral-500 hover:text-neutral-950">All work</a>
|
||||
</div>
|
||||
<div class="mx-auto grid max-w-7xl gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{{range .Projects}}
|
||||
<a href="/projects/{{.Slug}}" class="group block">
|
||||
<div class="aspect-square overflow-hidden bg-neutral-200">
|
||||
<img src="{{.CoverImage}}" alt="{{.Title}}" class="h-full w-full object-cover transition duration-500 group-hover:scale-105">
|
||||
</div>
|
||||
<div class="mt-3 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium">{{.Title}}</h3>
|
||||
<p class="text-sm text-neutral-500">{{.Location}}</p>
|
||||
</div>
|
||||
<p class="text-sm text-neutral-500">{{.Year}}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{template "footer" .}}
|
||||
<div id="overlay-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div data-overlay class="fixed inset-0 z-50 flex items-center justify-center bg-black/95 p-4 text-white md:p-8" role="dialog" aria-modal="true" aria-label="{{.Project.Title}} image viewer">
|
||||
<button type="button" data-overlay-close class="absolute right-4 top-4 z-10 rounded-full border border-white/30 px-4 py-2 text-sm uppercase tracking-[0.16em] text-white hover:bg-white hover:text-black">Close</button>
|
||||
<img src="{{.Image.Path}}" alt="{{.Image.Caption}}" class="max-h-full max-w-full object-contain">
|
||||
{{if .Image.Caption}}
|
||||
<p class="absolute bottom-4 left-4 right-4 text-center text-sm text-white/70">{{.Image.Caption}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
{{template "head" .}}
|
||||
{{template "site_header" .}}
|
||||
<main class="pt-28 md:pt-36">
|
||||
<section class="mx-auto max-w-7xl px-5 md:px-8">
|
||||
<p class="mb-4 text-sm uppercase tracking-[0.2em] text-neutral-500">{{.Project.Category}}</p>
|
||||
<div class="grid gap-8 md:grid-cols-[1.2fr_0.8fr] md:items-end">
|
||||
<h1 class="text-5xl font-semibold leading-none md:text-8xl">{{.Project.Title}}</h1>
|
||||
<div class="grid grid-cols-3 gap-4 border-t border-neutral-200 pt-4 text-sm">
|
||||
<div><p class="text-neutral-500">Location</p><p>{{.Project.Location}}</p></div>
|
||||
<div><p class="text-neutral-500">Year</p><p>{{.Project.Year}}</p></div>
|
||||
<div><p class="text-neutral-500">Type</p><p>{{.Project.Category}}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-10 max-w-3xl text-xl leading-relaxed text-neutral-600">{{.Project.Description}}</p>
|
||||
</section>
|
||||
|
||||
<section class="mx-auto mt-14 max-w-7xl px-5 pb-24 md:px-8">
|
||||
<div class="grid gap-5">
|
||||
{{range .Project.Images}}
|
||||
<button type="button" class="group block w-full text-left" hx-get="/projects/{{$.Project.Slug}}/images/{{.ID}}/overlay" hx-target="#overlay-root" hx-swap="innerHTML transition:true">
|
||||
<img src="{{.Path}}" alt="{{.Caption}}" class="max-h-[86vh] w-full bg-neutral-200 object-cover transition duration-500 group-hover:brightness-90">
|
||||
{{if .Caption}}<span class="mt-2 block text-sm text-neutral-500">{{.Caption}}</span>{{end}}
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{template "footer" .}}
|
||||
<div id="overlay-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
{{template "head" .}}
|
||||
{{template "site_header" .}}
|
||||
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
|
||||
<div class="mb-10 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p class="mb-3 text-sm uppercase tracking-[0.2em] text-neutral-500">Selected work</p>
|
||||
<h1 class="text-5xl font-semibold md:text-7xl">Projects</h1>
|
||||
</div>
|
||||
<p class="max-w-md text-neutral-600">A visual index of architectural and interior design work.</p>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
|
||||
{{range .Projects}}
|
||||
<a href="/projects/{{.Slug}}" class="group block">
|
||||
<div class="aspect-square overflow-hidden bg-neutral-200">
|
||||
<img src="{{.CoverImage}}" alt="{{.Title}}" class="h-full w-full object-cover transition duration-500 group-hover:scale-105">
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<h2 class="text-lg font-medium">{{.Title}}</h2>
|
||||
<p class="text-sm text-neutral-500">{{.Location}} · {{.Year}}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer" .}}
|
||||
<div id="overlay-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user