Updated version rollout
All checks were successful
Publish / Test, build, and push image (push) Successful in 4m8s
All checks were successful
Publish / Test, build, and push image (push) Successful in 4m8s
This commit is contained in:
parent
c4d199e20a
commit
7079e32a5f
316
ROUND_3.md
Normal file
316
ROUND_3.md
Normal file
@ -0,0 +1,316 @@
|
||||
# Round 3 Plan: Solo Architecture Studio Redesign
|
||||
|
||||
## Context
|
||||
|
||||
The current application is a compact server-rendered Go site with SQLite storage, Tailwind-loaded templates, HTMX navigation, image uploads, a password-protected admin area, project/gallery CRUD, a single editable `site_content` row, and a simple contact request inbox.
|
||||
|
||||
The public interface currently has:
|
||||
|
||||
- Home page with hero, intro statement, and featured projects.
|
||||
- Projects index.
|
||||
- Project detail pages with image overlays.
|
||||
- About page that also contains the only public contact form.
|
||||
- No dedicated Services page.
|
||||
- No dedicated Contact page.
|
||||
|
||||
For a small single-person architecture practice, the redesign should not become a full agency website or heavy CMS. The site needs to build trust quickly, show a clear design position, make project work easy to inspect, and collect useful enquiries without making the owner maintain many pages.
|
||||
|
||||
## What From The Recommendation Fits
|
||||
|
||||
- Home page: relevant and already mostly present, but it needs stronger positioning, a clearer call to action, and a better preview of services/process.
|
||||
- About / Studio page: highly relevant. For a solo practice, this should be one of the strongest trust-building pages because clients are hiring the person, not a large firm.
|
||||
- Services page: relevant, but should be concise. A long list like architecture, interiors, renovation, consultation, space planning, furniture, project management, and 3D visualization may feel inflated for a one-person studio unless each item is genuinely offered.
|
||||
- Contact page: relevant. The current form is buried on About, and the form does not pre-qualify leads.
|
||||
|
||||
## What To Avoid
|
||||
|
||||
- Do not create a large multi-page agency structure with too many thin pages.
|
||||
- Do not add a complex awards/publications system unless the owner has real content to maintain.
|
||||
- Do not overstate capabilities with every possible design service.
|
||||
- Do not add map/social integrations until the real contact content exists.
|
||||
- Do not build a full page builder. The admin should remain simple and opinionated.
|
||||
|
||||
## Proposed Public Site Structure
|
||||
|
||||
### 1. Home
|
||||
|
||||
Purpose: communicate the studio position quickly and send visitors to projects or enquiry.
|
||||
|
||||
Recommended sections:
|
||||
|
||||
- Full-viewport image-led hero, using a real project image when available.
|
||||
- Positioning line such as "Residential architecture and interiors in London" rather than generic portfolio wording.
|
||||
- One primary CTA: "Start an enquiry".
|
||||
- One secondary CTA: "View projects".
|
||||
- Short studio statement, 2-3 sentences.
|
||||
- Featured projects, limited to 3-4.
|
||||
- Compact services preview, 3 service groups maximum.
|
||||
- Brief process preview, 3 steps maximum.
|
||||
- Short about preview with portrait and link to Studio.
|
||||
|
||||
Current implementation impact:
|
||||
|
||||
- Keep the existing Home route.
|
||||
- Rework `home.html` layout and copy fields.
|
||||
- Continue using featured projects from the existing project model.
|
||||
- Add admin-editable CTA text/link and positioning fields.
|
||||
|
||||
### 2. Studio
|
||||
|
||||
Purpose: make the solo practitioner credible, specific, and approachable.
|
||||
|
||||
This should replace or rename the current About page to "Studio". The route can remain `/about` for compatibility, but the navigation label should become `Studio`.
|
||||
|
||||
Recommended sections:
|
||||
|
||||
- Portrait or working photo.
|
||||
- Name, role, and location.
|
||||
- Short personal narrative.
|
||||
- Design philosophy.
|
||||
- Approach/process.
|
||||
- Credentials, registrations, selected experience, or publications as a simple text block.
|
||||
- Contact CTA.
|
||||
|
||||
Current implementation impact:
|
||||
|
||||
- Keep the existing about content fields but expand them.
|
||||
- Add fields for philosophy, approach, credentials, and availability/service area.
|
||||
- Remove the main contact form from this page or reduce it to a CTA linking to Contact.
|
||||
|
||||
### 3. Services
|
||||
|
||||
Purpose: help visitors self-qualify without making the practice look larger than it is.
|
||||
|
||||
Recommended services for a solo architecture company:
|
||||
|
||||
- Residential architecture.
|
||||
- Renovation and extensions.
|
||||
- Interior architecture and spatial planning.
|
||||
- Early-stage consultation.
|
||||
|
||||
Optional only if accurate:
|
||||
|
||||
- Planning support.
|
||||
- 3D visualization.
|
||||
- Furniture and finish selection.
|
||||
- Project coordination.
|
||||
|
||||
Recommended sections:
|
||||
|
||||
- Service overview.
|
||||
- 3-4 editable service cards.
|
||||
- "How projects work" process section.
|
||||
- Typical timeline ranges.
|
||||
- Geographic availability.
|
||||
- Small FAQ.
|
||||
- CTA to Contact.
|
||||
|
||||
Current implementation impact:
|
||||
|
||||
- Add `GET /services`.
|
||||
- Add a `services.html` template.
|
||||
- Add admin support for editable services and FAQs.
|
||||
- For the first implementation, services can be stored as structured rows rather than hard-coded copy.
|
||||
|
||||
### 4. Contact
|
||||
|
||||
Purpose: create a proper conversion point and collect enough information to assess fit.
|
||||
|
||||
Recommended sections:
|
||||
|
||||
- Contact form.
|
||||
- Email, phone, location.
|
||||
- Short expectation statement, for example response time or preferred project types.
|
||||
- Optional social links.
|
||||
- Optional map later, not required now.
|
||||
|
||||
Inquiry questionnaire fields:
|
||||
|
||||
- Name.
|
||||
- Email.
|
||||
- Phone, optional.
|
||||
- Project type.
|
||||
- Location.
|
||||
- Budget range.
|
||||
- Timeline.
|
||||
- Message.
|
||||
|
||||
Current implementation impact:
|
||||
|
||||
- Add `GET /contact`.
|
||||
- Keep `POST /contact`, but expand the stored fields.
|
||||
- Update admin contact inbox to show the structured enquiry details.
|
||||
- Add validation for required fields and length limits.
|
||||
|
||||
## Proposed Navigation
|
||||
|
||||
Use five top-level links:
|
||||
|
||||
- Projects
|
||||
- Studio
|
||||
- Services
|
||||
- Contact
|
||||
|
||||
Home remains the logo link. This keeps navigation focused and avoids a marketing-heavy structure.
|
||||
|
||||
## Backend And Data Plan
|
||||
|
||||
### Site Content
|
||||
|
||||
The existing `site_content` row is good for global editable content, but it needs more fields:
|
||||
|
||||
- `site_name`
|
||||
- `positioning`
|
||||
- `hero_cta_label`
|
||||
- `hero_cta_url`
|
||||
- `secondary_cta_label`
|
||||
- `secondary_cta_url`
|
||||
- `studio_philosophy`
|
||||
- `studio_approach`
|
||||
- `studio_credentials`
|
||||
- `service_area`
|
||||
- `response_note`
|
||||
|
||||
These can be added through a migration while preserving current data.
|
||||
|
||||
### Services
|
||||
|
||||
Add a `services` table:
|
||||
|
||||
- `id`
|
||||
- `title`
|
||||
- `summary`
|
||||
- `details`
|
||||
- `position`
|
||||
- `active`
|
||||
|
||||
This keeps the Services page editable without turning it into a page builder.
|
||||
|
||||
### FAQs
|
||||
|
||||
Add a `faqs` table:
|
||||
|
||||
- `id`
|
||||
- `question`
|
||||
- `answer`
|
||||
- `position`
|
||||
- `active`
|
||||
|
||||
Use FAQs mainly on Services and optionally Contact.
|
||||
|
||||
### Contact Requests
|
||||
|
||||
Expand `contact_requests`:
|
||||
|
||||
- `phone`
|
||||
- `project_type`
|
||||
- `project_location`
|
||||
- `budget_range`
|
||||
- `timeline`
|
||||
- `status`
|
||||
- `notes`
|
||||
|
||||
`status` can start simple: `new`, `reviewed`, `archived`. `notes` is private admin-only text.
|
||||
|
||||
### Projects
|
||||
|
||||
The current project model is usable. Consider adding only:
|
||||
|
||||
- `summary` for cards and homepage previews.
|
||||
- `scope` for project detail metadata.
|
||||
- `status` or `completion_stage` if the studio wants to show built/in progress/concept.
|
||||
- `position` for manual ordering.
|
||||
|
||||
Manual ordering matters more than created-at ordering for a portfolio.
|
||||
|
||||
## Admin Plan
|
||||
|
||||
Keep the tabbed admin from Round 2 and add two tabs:
|
||||
|
||||
- Main
|
||||
- Projects
|
||||
- Studio
|
||||
- Services
|
||||
- Contact
|
||||
|
||||
Recommended ownership:
|
||||
|
||||
- Main: home hero, positioning, intro, CTA labels/links, hero image.
|
||||
- Projects: project CRUD, gallery uploads, featured flag, ordering.
|
||||
- Studio: portrait, name, role, bio, philosophy, approach, credentials.
|
||||
- Services: service rows, process copy, FAQ rows.
|
||||
- Contact: public contact details, enquiry inbox, enquiry status, private notes.
|
||||
|
||||
The admin should stay form-based. Avoid rich text editing in this round unless there is a clear content requirement.
|
||||
|
||||
## Interface Direction
|
||||
|
||||
The current visual direction is image-led, quiet, and minimalist. That fits architecture, but the redesign should make the site feel more intentional and less like a generic template.
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- Use generous image layouts for project pages.
|
||||
- Keep typography restrained but improve hierarchy.
|
||||
- Make the homepage less sparse by adding actionable service/process content.
|
||||
- Use project facts and captions to make work inspectable.
|
||||
- Keep color neutral, but avoid a flat all-neutral interface by using material-inspired accents such as stone, clay, brass, or muted green in small amounts.
|
||||
- Do not use decorative cards as the main layout. Use full-width sections, grids, and strong image composition.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Public IA And Contact
|
||||
|
||||
- Add `/contact` page.
|
||||
- Move the enquiry form there.
|
||||
- Expand contact request fields.
|
||||
- Add navigation item.
|
||||
- Update admin contact inbox.
|
||||
|
||||
This creates immediate business value and fixes the weakest current conversion path.
|
||||
|
||||
### Phase 2: Studio And Home Rework
|
||||
|
||||
- Rename About navigation to Studio.
|
||||
- Expand Studio content fields.
|
||||
- Rework homepage sections and CTAs.
|
||||
- Add home services/process preview using simple editable fields or seeded service rows.
|
||||
|
||||
This improves trust and positioning without a large backend expansion.
|
||||
|
||||
### Phase 3: Services
|
||||
|
||||
- Add `/services`.
|
||||
- Add services and FAQs tables.
|
||||
- Add admin Services tab.
|
||||
- Add process/timeline/geography content.
|
||||
|
||||
This helps visitors self-qualify and reduces repetitive pre-sales explanations.
|
||||
|
||||
### Phase 4: Project Depth
|
||||
|
||||
- Add project summary/scope/status/manual ordering.
|
||||
- Improve project detail pages with clearer metadata and richer captions.
|
||||
- Add admin ordering controls.
|
||||
|
||||
This is valuable after the lead path and positioning are stronger.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
- Add route tests for `/`, `/projects`, `/projects/{slug}`, `/about`, `/services`, and `/contact`.
|
||||
- Add form tests for expanded contact validation and persistence.
|
||||
- Add migration tests for new columns/tables.
|
||||
- Add admin tests for new tabs and update actions.
|
||||
- Manually verify HTMX navigation still works with direct URL fallback.
|
||||
- Manually verify mobile layouts for Home, Studio, Services, Contact, and Project detail.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the public label be "Studio" while keeping `/about`, or should the route become `/studio` with `/about` redirecting?
|
||||
- Which services are genuinely offered by the practitioner today?
|
||||
- Does the studio want to show a phone number publicly, or keep initial contact email/form-only?
|
||||
- Are there real awards, registrations, or publications to show, or should credentials remain a simple text block?
|
||||
- Should contact requests trigger email notification, or is the admin inbox enough for now?
|
||||
|
||||
## Recommended Next Step
|
||||
|
||||
Start with Phase 1. A dedicated Contact page with a structured enquiry form is the highest-value backend and interface improvement, and it gives the later Home, Studio, and Services pages a clear conversion destination.
|
||||
244
ROUND_4.md
Normal file
244
ROUND_4.md
Normal file
@ -0,0 +1,244 @@
|
||||
# Round 4 Plan: Public Shell, HTMX Partials, and Smoother Navigation
|
||||
|
||||
## Goal
|
||||
|
||||
Refactor the public interface so `base.html` is the single public shell for header, footer, DaisyUI drawer, overlay root, and the stable main content container. Public pages should render either as a full document on direct visits or as main-content partials during HTMX navigation.
|
||||
|
||||
This keeps the current architecture portfolio style and functionality while making page structure easier to maintain.
|
||||
|
||||
## Current State
|
||||
|
||||
- Public pages call `{{template "site_start" .}}`, then render their own `<main>`, then call `{{template "site_end" .}}`.
|
||||
- Public navigation generally uses `hx-boost="true"` with `hx-target="body"` and full `body` replacement.
|
||||
- The mobile sidebar now uses DaisyUI drawer structure.
|
||||
- Admin already has a better partial pattern: full page for direct requests, partial panel for HTMX requests, plus out-of-band tab updates.
|
||||
|
||||
## Target Structure
|
||||
|
||||
Use `base.html` as the public shell.
|
||||
|
||||
Recommended template ownership:
|
||||
|
||||
- `base.html`
|
||||
- `head`
|
||||
- full public document shell
|
||||
- header
|
||||
- desktop nav
|
||||
- DaisyUI mobile drawer
|
||||
- footer
|
||||
- `#main-content`
|
||||
- `#overlay-root`
|
||||
- OOB nav fragments
|
||||
- Page templates
|
||||
- define only page content partials, for example:
|
||||
- `home_content`
|
||||
- `projects_content`
|
||||
- `project_content`
|
||||
- `studio_content`
|
||||
- `services_content`
|
||||
- `contact_content`
|
||||
|
||||
## Rendering Model
|
||||
|
||||
### Direct Page Load
|
||||
|
||||
Normal browser requests render the full shell:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>...</head>
|
||||
<body>
|
||||
<div class="drawer drawer-end">
|
||||
...
|
||||
<main id="main-content">
|
||||
page content
|
||||
</main>
|
||||
...
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### HTMX Navigation
|
||||
|
||||
HTMX requests return:
|
||||
|
||||
- the new `#main-content` element
|
||||
- out-of-band desktop nav active state
|
||||
- out-of-band drawer nav active state
|
||||
- optionally an out-of-band document title update if needed later
|
||||
|
||||
This mirrors the admin partial strategy.
|
||||
|
||||
## HTMX Navigation Changes
|
||||
|
||||
Change public internal navigation from body replacement:
|
||||
|
||||
```html
|
||||
hx-target="body"
|
||||
hx-swap="outerHTML transition:true"
|
||||
```
|
||||
|
||||
to main-content replacement:
|
||||
|
||||
```html
|
||||
hx-target="#main-content"
|
||||
hx-swap="outerHTML transition:true"
|
||||
hx-push-url="true"
|
||||
```
|
||||
|
||||
The header, drawer, footer, and overlay root remain stable across page transitions.
|
||||
|
||||
## Active Navigation
|
||||
|
||||
Because only the main content will swap, active navigation state needs to update separately.
|
||||
|
||||
Use out-of-band fragments:
|
||||
|
||||
- `#site-desktop-nav`
|
||||
- `#site-drawer-nav`
|
||||
|
||||
The full shell and HTMX partial responses both render nav from the same template definitions, avoiding duplicated active-state logic.
|
||||
|
||||
## Drawer Behavior
|
||||
|
||||
Keep the DaisyUI drawer:
|
||||
|
||||
- `drawer drawer-end`
|
||||
- `drawer-toggle`
|
||||
- `drawer-content`
|
||||
- `drawer-side`
|
||||
- `drawer-overlay`
|
||||
|
||||
Improvements:
|
||||
|
||||
- Keep the drawer as part of the stable shell, not page content.
|
||||
- Close drawer after clicking a drawer nav link.
|
||||
- Close drawer on Escape.
|
||||
- Keep the matted glass visual:
|
||||
- `bg-neutral-950/45`
|
||||
- `backdrop-blur-xl`
|
||||
- white text
|
||||
- active item underlined
|
||||
|
||||
## Smoother Transitions
|
||||
|
||||
### Page Content
|
||||
|
||||
Apply transitions to `#main-content`, not the full body.
|
||||
|
||||
Recommended CSS:
|
||||
|
||||
- old content: slight fade out and 4-6px downward motion
|
||||
- new content: fade in and return to zero offset
|
||||
- short duration, around 160-220ms
|
||||
- respect `prefers-reduced-motion`
|
||||
|
||||
The existing View Transition CSS can be adjusted so:
|
||||
|
||||
- `#main-content` has `view-transition-name: main-content`
|
||||
- root transitions are less dominant or removed for public navigation
|
||||
- admin panel transitions remain independent
|
||||
|
||||
### Drawer
|
||||
|
||||
DaisyUI handles the structural entry/exit, but add a small custom polish layer if needed:
|
||||
|
||||
- slightly longer transform duration, around 240ms
|
||||
- eased slide-in/out
|
||||
- backdrop fade around 180-220ms
|
||||
|
||||
Avoid custom JavaScript animation. Prefer CSS.
|
||||
|
||||
## Backend Changes
|
||||
|
||||
Add a public render helper similar to admin rendering:
|
||||
|
||||
```go
|
||||
func (s *Server) renderPublic(w http.ResponseWriter, r *http.Request, fullTemplate, partialTemplate string, data pageData)
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- normal request: render full shell with the correct page content
|
||||
- HTMX request: render partial content plus OOB nav fragments
|
||||
|
||||
Each public handler should call `renderPublic` instead of `render`.
|
||||
|
||||
## Template Naming Plan
|
||||
|
||||
Suggested full/partial template names:
|
||||
|
||||
- `home.html` full shell, `home_partial.html`
|
||||
- `projects.html` full shell, `projects_partial.html`
|
||||
- `project.html` full shell, `project_partial.html`
|
||||
- `about.html` full shell, `about_partial.html`
|
||||
- `services.html` full shell, `services_partial.html`
|
||||
- `contact.html` full shell, `contact_partial.html`
|
||||
|
||||
Alternatively, each page file can define:
|
||||
|
||||
- `page_full`
|
||||
- `page_partial`
|
||||
- `page_content`
|
||||
|
||||
Pick one convention and use it everywhere.
|
||||
|
||||
## Preserve Current Functionality
|
||||
|
||||
Must continue working:
|
||||
|
||||
- direct URL visits
|
||||
- browser back/forward with pushed URLs
|
||||
- non-JavaScript navigation fallback
|
||||
- project image overlay HTMX requests
|
||||
- contact form HTMX submission
|
||||
- admin tab partial behavior
|
||||
- health endpoints
|
||||
- DaisyUI mobile drawer
|
||||
|
||||
## Testing Plan
|
||||
|
||||
Update or add tests for:
|
||||
|
||||
- direct public routes return full HTML document
|
||||
- HTMX public route requests return partial content, not full document
|
||||
- HTMX public partial responses include OOB nav fragments
|
||||
- active nav state updates for each page
|
||||
- project image overlay still returns only overlay fragment
|
||||
- contact form submission still returns only `contact_result.html`
|
||||
- existing admin HTMX tests still pass
|
||||
|
||||
Manual checks:
|
||||
|
||||
- mobile drawer opens/closes on every public page
|
||||
- drawer closes after nav click
|
||||
- drawer closes with Escape
|
||||
- page transitions feel smooth on desktop and mobile
|
||||
- reduced-motion setting disables meaningful animation
|
||||
- browser back/forward updates main content and nav state
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Refactor `base.html` into the full public shell.
|
||||
2. Convert one page, preferably Home, to full/partial/content templates.
|
||||
3. Add `renderPublic`.
|
||||
4. Convert remaining public pages.
|
||||
5. Add OOB nav fragments.
|
||||
6. Update HTMX targets in header, drawer, and internal CTAs.
|
||||
7. Adjust transition CSS for `#main-content`.
|
||||
8. Add drawer close-on-link/Escape behavior.
|
||||
9. Update tests.
|
||||
10. Run full test suite and manually verify key routes.
|
||||
|
||||
## Risks
|
||||
|
||||
- OOB nav fragments can become noisy if duplicated in every page template. Keep them in `base.html`.
|
||||
- Contact form and overlay requests should not use `renderPublic`; they should keep returning small fragments.
|
||||
- Page title will not automatically update if only `#main-content` swaps. This can be handled later with an OOB `<title>` strategy or small JS.
|
||||
- DaisyUI CDN use is acceptable for now, but a proper Tailwind/DaisyUI build pipeline would be better for production performance and version control.
|
||||
|
||||
## Recommended Scope For Round 4
|
||||
|
||||
Implement the public shell and main-content HTMX navigation first. Do not refactor admin forms or public content sections in the same round. Keep this round focused on layout architecture, navigation maintainability, and smoother transitions.
|
||||
@ -19,7 +19,7 @@ func (s *Server) home(w http.ResponseWriter, r *http.Request) {
|
||||
s.error(w, err)
|
||||
return
|
||||
}
|
||||
s.render(w, "home.html", pageData{Title: content.HeroTitle, Active: "home", Content: content, Projects: projects, CurrentPath: r.URL.Path})
|
||||
s.renderPublic(w, r, "home.html", "home_partial.html", pageData{Title: content.HeroTitle, Active: "home", Content: content, Projects: projects, CurrentPath: r.URL.Path})
|
||||
}
|
||||
|
||||
func (s *Server) projects(w http.ResponseWriter, r *http.Request) {
|
||||
@ -33,7 +33,7 @@ func (s *Server) projects(w http.ResponseWriter, r *http.Request) {
|
||||
s.error(w, err)
|
||||
return
|
||||
}
|
||||
s.render(w, "projects.html", pageData{Title: "Projects", Active: "projects", Content: content, Projects: projects, CurrentPath: r.URL.Path})
|
||||
s.renderPublic(w, r, "projects.html", "projects_partial.html", pageData{Title: "Projects", Active: "projects", Content: content, Projects: projects, CurrentPath: r.URL.Path})
|
||||
}
|
||||
|
||||
func (s *Server) projectDetail(w http.ResponseWriter, r *http.Request) {
|
||||
@ -51,7 +51,7 @@ func (s *Server) projectDetail(w http.ResponseWriter, r *http.Request) {
|
||||
s.error(w, err)
|
||||
return
|
||||
}
|
||||
s.render(w, "project.html", pageData{Title: project.Title, Active: "projects", Content: content, Project: project, CurrentPath: r.URL.Path})
|
||||
s.renderPublic(w, r, "project.html", "project_partial.html", pageData{Title: project.Title, Active: "projects", Content: content, Project: project, CurrentPath: r.URL.Path})
|
||||
}
|
||||
|
||||
func (s *Server) projectImageOverlay(w http.ResponseWriter, r *http.Request) {
|
||||
@ -78,7 +78,7 @@ func (s *Server) about(w http.ResponseWriter, r *http.Request) {
|
||||
s.error(w, err)
|
||||
return
|
||||
}
|
||||
s.render(w, "about.html", pageData{Title: "Studio", Active: "about", Content: content, CurrentPath: r.URL.Path})
|
||||
s.renderPublic(w, r, "about.html", "about_partial.html", pageData{Title: "Studio", Active: "about", Content: content, CurrentPath: r.URL.Path})
|
||||
}
|
||||
|
||||
func (s *Server) services(w http.ResponseWriter, r *http.Request) {
|
||||
@ -97,7 +97,7 @@ func (s *Server) services(w http.ResponseWriter, r *http.Request) {
|
||||
s.error(w, err)
|
||||
return
|
||||
}
|
||||
s.render(w, "services.html", pageData{Title: "Services", Active: "services", Content: content, Services: services, FAQs: faqs, CurrentPath: r.URL.Path})
|
||||
s.renderPublic(w, r, "services.html", "services_partial.html", pageData{Title: "Services", Active: "services", Content: content, Services: services, FAQs: faqs, CurrentPath: r.URL.Path})
|
||||
}
|
||||
|
||||
func (s *Server) contactPage(w http.ResponseWriter, r *http.Request) {
|
||||
@ -106,7 +106,7 @@ func (s *Server) contactPage(w http.ResponseWriter, r *http.Request) {
|
||||
s.error(w, err)
|
||||
return
|
||||
}
|
||||
s.render(w, "contact.html", pageData{Title: "Contact", Active: "contact", Content: content, CurrentPath: r.URL.Path})
|
||||
s.renderPublic(w, r, "contact.html", "contact_partial.html", pageData{Title: "Contact", Active: "contact", Content: content, CurrentPath: r.URL.Path})
|
||||
}
|
||||
|
||||
func (s *Server) contact(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -24,6 +24,48 @@ func TestPublicRoutes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicRoutesRenderFullShellOnDirectLoad(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/services", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
srv.Routes().ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected ok, got %d", rec.Code)
|
||||
}
|
||||
body, _ := io.ReadAll(rec.Result().Body)
|
||||
text := string(body)
|
||||
for _, want := range []string{"<!doctype html>", `class="drawer drawer-end"`, `id="main-content"`, `id="site-drawer"`} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("direct public route missing %q: %s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicHTMXRoutesReturnMainContentPartial(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/services", nil)
|
||||
req.Header.Set("HX-Request", "true")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
srv.Routes().ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected ok, got %d", rec.Code)
|
||||
}
|
||||
body, _ := io.ReadAll(rec.Result().Body)
|
||||
text := string(body)
|
||||
if strings.Contains(text, "<!doctype html>") || strings.Contains(text, `class="drawer drawer-end"`) {
|
||||
t.Fatalf("expected partial response, got full document: %s", text)
|
||||
}
|
||||
for _, want := range []string{`id="main-content"`, `id="site-header"`, `id="site-drawer-nav"`, `hx-swap-oob="true"`} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("HTMX public route missing %q: %s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicesRouteRendersServicesAndFAQs(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/services", nil)
|
||||
|
||||
@ -25,6 +25,14 @@ func (s *Server) renderAdmin(w http.ResponseWriter, r *http.Request, fullTemplat
|
||||
s.render(w, fullTemplate, data)
|
||||
}
|
||||
|
||||
func (s *Server) renderPublic(w http.ResponseWriter, r *http.Request, fullTemplate, partialTemplate string, data pageData) {
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
s.render(w, partialTemplate, data)
|
||||
return
|
||||
}
|
||||
s.render(w, fullTemplate, data)
|
||||
}
|
||||
|
||||
func (s *Server) error(w http.ResponseWriter, err error) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@ -8,6 +8,31 @@
|
||||
updateHeader();
|
||||
window.addEventListener("scroll", updateHeader, { passive: true });
|
||||
|
||||
function currentTheme() {
|
||||
try {
|
||||
const saved = localStorage.getItem("archi-theme");
|
||||
if (saved === "dark" || saved === "light") return saved;
|
||||
} catch (_) {}
|
||||
return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
const normalized = theme === "dark" ? "dark" : "light";
|
||||
document.documentElement.setAttribute("data-theme", normalized);
|
||||
document.querySelectorAll("[data-theme-toggle]").forEach(function (input) {
|
||||
input.checked = normalized === "dark";
|
||||
});
|
||||
try {
|
||||
localStorage.setItem("archi-theme", normalized);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function syncThemeControls() {
|
||||
setTheme(currentTheme());
|
||||
}
|
||||
|
||||
syncThemeControls();
|
||||
|
||||
function closeOverlay() {
|
||||
const root = document.getElementById("overlay-root");
|
||||
if (root) root.innerHTML = "";
|
||||
@ -25,13 +50,25 @@
|
||||
});
|
||||
|
||||
document.addEventListener("htmx:afterSettle", updateHeader);
|
||||
document.addEventListener("htmx:afterSettle", syncThemeControls);
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (event.target.closest("[data-drawer-link]")) {
|
||||
const drawer = document.getElementById("site-drawer");
|
||||
if (drawer) drawer.checked = false;
|
||||
}
|
||||
if (event.target.matches("[data-overlay], [data-overlay-close]")) {
|
||||
closeOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("change", function (event) {
|
||||
const toggle = event.target.closest("[data-theme-toggle]");
|
||||
if (toggle) {
|
||||
setTheme(toggle.checked ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape") {
|
||||
const drawer = document.getElementById("site-drawer");
|
||||
|
||||
@ -6,6 +6,61 @@ body {
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] body,
|
||||
html[data-theme="dark"] .drawer-content {
|
||||
background: #0f0f0e;
|
||||
color: #f5f5f4;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .bg-neutral-50 {
|
||||
background-color: #0f0f0e !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .bg-white {
|
||||
background-color: #171716 !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .bg-neutral-950 {
|
||||
background-color: #050505 !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .bg-neutral-200 {
|
||||
background-color: #2a2926 !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .text-neutral-950,
|
||||
html[data-theme="dark"] .text-neutral-800 {
|
||||
color: #f5f5f4 !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .text-neutral-700,
|
||||
html[data-theme="dark"] .text-neutral-600,
|
||||
html[data-theme="dark"] .text-neutral-500 {
|
||||
color: #c7c2b8 !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .text-neutral-400 {
|
||||
color: #a8a29a !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .border-neutral-300,
|
||||
html[data-theme="dark"] .border-neutral-200 {
|
||||
border-color: rgba(245, 245, 244, 0.16) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] input,
|
||||
html[data-theme="dark"] textarea,
|
||||
html[data-theme="dark"] select {
|
||||
background-color: #111110;
|
||||
border-color: rgba(245, 245, 244, 0.22);
|
||||
color: #f5f5f4;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] input::placeholder,
|
||||
html[data-theme="dark"] textarea::placeholder {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
appearance: none;
|
||||
border-radius: 0;
|
||||
@ -30,6 +85,32 @@ body {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] [data-site-header] {
|
||||
background: rgba(15, 15, 14, 0.95) !important;
|
||||
color: #f5f5f4 !important;
|
||||
box-shadow: 0 1px 0 rgba(245, 245, 244, 0.12);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] [data-site-header].is-compact {
|
||||
background: rgba(15, 15, 14, 0.95) !important;
|
||||
color: #f5f5f4 !important;
|
||||
box-shadow: 0 1px 0 rgba(245, 245, 244, 0.12);
|
||||
}
|
||||
|
||||
#main-content {
|
||||
view-transition-name: main-content;
|
||||
}
|
||||
|
||||
.drawer-side > aside {
|
||||
transition-duration: 260ms;
|
||||
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.drawer-overlay {
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
|
||||
@keyframes page-fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
@ -90,6 +171,14 @@ body {
|
||||
animation: 180ms ease both panel-fade-in;
|
||||
}
|
||||
|
||||
::view-transition-old(main-content) {
|
||||
animation: 150ms ease both panel-fade-out;
|
||||
}
|
||||
|
||||
::view-transition-new(main-content) {
|
||||
animation: 210ms ease both panel-fade-in;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
::before,
|
||||
@ -99,6 +188,8 @@ body {
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root),
|
||||
::view-transition-old(main-content),
|
||||
::view-transition-new(main-content),
|
||||
::view-transition-old(admin-panel),
|
||||
::view-transition-new(admin-panel) {
|
||||
animation-duration: 1ms;
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
{{template "site_start" .}}
|
||||
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
|
||||
{{define "about.html"}}
|
||||
{{template "public_shell_start" .}}
|
||||
{{template "about_content" .}}
|
||||
{{template "public_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "about_partial.html"}}
|
||||
{{template "public_nav_oob" .}}
|
||||
{{template "about_content" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "about_content"}}
|
||||
<main id="main-content" 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">
|
||||
@ -60,9 +71,9 @@
|
||||
<h2 class="text-3xl font-semibold">Discuss a project</h2>
|
||||
<div class="max-w-2xl">
|
||||
<p class="text-lg leading-relaxed text-neutral-600">Share the project type, location, budget range, and timeline so the studio can assess whether the work is a good fit.</p>
|
||||
<a href="/contact" class="mt-6 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Start an enquiry</a>
|
||||
<a href="/contact" class="mt-6 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Start an enquiry</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{template "site_end" .}}
|
||||
{{end}}
|
||||
|
||||
@ -5,6 +5,16 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{if .Title}}{{.Title}} | {{end}}Archi Folio</title>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var theme = localStorage.getItem("archi-theme");
|
||||
if (theme === "dark" || theme === "light") {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
@ -14,7 +24,7 @@
|
||||
<body class="bg-neutral-50 text-neutral-950 antialiased">
|
||||
{{end}}
|
||||
|
||||
{{define "site_start"}}
|
||||
{{define "public_shell_start"}}
|
||||
{{template "head" .}}
|
||||
<div class="drawer drawer-end">
|
||||
<input id="site-drawer" type="checkbox" class="drawer-toggle">
|
||||
@ -23,15 +33,24 @@
|
||||
{{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}}">
|
||||
<header id="site-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}}">
|
||||
{{template "site_header_inner" .}}
|
||||
</header>
|
||||
{{end}}
|
||||
|
||||
{{define "site_header_oob"}}
|
||||
<header id="site-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}}" hx-swap-oob="true">
|
||||
{{template "site_header_inner" .}}
|
||||
</header>
|
||||
{{end}}
|
||||
|
||||
{{define "site_header_inner"}}
|
||||
<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="hidden items-center gap-4 text-sm uppercase tracking-[0.18em] md:flex 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">Studio</a>
|
||||
<a class="hover:opacity-60 {{if eq .Active "services"}}font-semibold{{end}}" href="/services">Services</a>
|
||||
<a class="hover:opacity-60 {{if eq .Active "contact"}}font-semibold{{end}}" href="/contact">Contact</a>
|
||||
</nav>
|
||||
<a href="/" class="text-xl font-semibold tracking-normal md:text-3xl" data-header-brand hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Archi Folio</a>
|
||||
<div class="hidden items-center gap-6 md:flex">
|
||||
{{template "site_desktop_nav" .}}
|
||||
{{template "theme_toggle" .}}
|
||||
</div>
|
||||
<label for="site-drawer" class="grid h-11 w-11 cursor-pointer place-items-center border border-current/30 md:hidden" aria-label="Open menu">
|
||||
<span class="grid gap-1.5">
|
||||
<span class="block h-px w-5 bg-current"></span>
|
||||
@ -40,7 +59,24 @@
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
{{end}}
|
||||
|
||||
{{define "site_desktop_nav"}}
|
||||
<nav id="site-desktop-nav" class="hidden items-center gap-4 text-sm uppercase tracking-[0.18em] md:flex md:gap-8" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="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">Studio</a>
|
||||
<a class="hover:opacity-60 {{if eq .Active "services"}}font-semibold{{end}}" href="/services">Services</a>
|
||||
<a class="hover:opacity-60 {{if eq .Active "contact"}}font-semibold{{end}}" href="/contact">Contact</a>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
{{define "site_desktop_nav_oob"}}
|
||||
<nav id="site-desktop-nav" class="hidden items-center gap-4 text-sm uppercase tracking-[0.18em] md:flex md:gap-8" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true" hx-swap-oob="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">Studio</a>
|
||||
<a class="hover:opacity-60 {{if eq .Active "services"}}font-semibold{{end}}" href="/services">Services</a>
|
||||
<a class="hover:opacity-60 {{if eq .Active "contact"}}font-semibold{{end}}" href="/contact">Contact</a>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
{{define "footer"}}
|
||||
@ -52,7 +88,24 @@
|
||||
</footer>
|
||||
{{end}}
|
||||
|
||||
{{define "site_end"}}
|
||||
{{define "theme_toggle"}}
|
||||
<label class="swap swap-rotate grid h-10 w-10 cursor-pointer place-items-center text-current" aria-label="Toggle dark theme">
|
||||
<input type="checkbox" value="dark" class="theme-controller" data-theme-toggle>
|
||||
<svg class="swap-off h-6 w-6 fill-current" aria-label="sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"></path>
|
||||
</svg>
|
||||
<svg class="swap-on h-6 w-6 fill-current" aria-label="moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"></path>
|
||||
</svg>
|
||||
</label>
|
||||
{{end}}
|
||||
|
||||
{{define "public_nav_oob"}}
|
||||
{{template "site_header_oob" .}}
|
||||
{{template "site_drawer_nav_oob" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "public_shell_end"}}
|
||||
{{template "footer" .}}
|
||||
<div id="overlay-root"></div>
|
||||
</div>
|
||||
@ -60,7 +113,9 @@
|
||||
<label for="site-drawer" aria-label="Close menu" class="drawer-overlay bg-neutral-950/45"></label>
|
||||
<aside class="flex min-h-full w-[min(22rem,86vw)] flex-col bg-neutral-950/45 text-white shadow-2xl backdrop-blur-xl">
|
||||
<div class="flex items-center justify-between border-b border-white/15 px-5 py-4">
|
||||
<a href="/" class="text-xl font-semibold text-white" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Archi Folio</a>
|
||||
<a href="/" class="text-xl font-semibold text-white" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true" data-drawer-link>Archi Folio</a>
|
||||
<div class="flex items-center gap-3">
|
||||
{{template "theme_toggle" .}}
|
||||
<label for="site-drawer" class="grid h-10 w-10 cursor-pointer place-items-center border border-white/25 text-white" aria-label="Close menu">
|
||||
<span class="relative block h-5 w-5">
|
||||
<span class="absolute left-0 top-1/2 block h-px w-5 rotate-45 bg-current"></span>
|
||||
@ -68,12 +123,8 @@
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<nav class="grid px-5 py-6 text-2xl font-medium text-white" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "projects"}}underline underline-offset-8{{end}}" href="/projects">Projects</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "about"}}underline underline-offset-8{{end}}" href="/about">Studio</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "services"}}underline underline-offset-8{{end}}" href="/services">Services</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "contact"}}underline underline-offset-8{{end}}" href="/contact">Contact</a>
|
||||
</nav>
|
||||
</div>
|
||||
{{template "site_drawer_nav" .}}
|
||||
<div class="mt-auto px-5 pb-6 text-sm leading-relaxed text-white">
|
||||
<p>{{.Content.Email}}</p>
|
||||
<p>{{.Content.Location}}</p>
|
||||
@ -84,3 +135,21 @@
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "site_drawer_nav"}}
|
||||
<nav id="site-drawer-nav" class="grid px-5 py-6 text-2xl font-medium text-white" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "projects"}}underline underline-offset-8{{end}}" href="/projects" data-drawer-link>Projects</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "about"}}underline underline-offset-8{{end}}" href="/about" data-drawer-link>Studio</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "services"}}underline underline-offset-8{{end}}" href="/services" data-drawer-link>Services</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "contact"}}underline underline-offset-8{{end}}" href="/contact" data-drawer-link>Contact</a>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
{{define "site_drawer_nav_oob"}}
|
||||
<nav id="site-drawer-nav" class="grid px-5 py-6 text-2xl font-medium text-white" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true" hx-swap-oob="true">
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "projects"}}underline underline-offset-8{{end}}" href="/projects" data-drawer-link>Projects</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "about"}}underline underline-offset-8{{end}}" href="/about" data-drawer-link>Studio</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "services"}}underline underline-offset-8{{end}}" href="/services" data-drawer-link>Services</a>
|
||||
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "contact"}}underline underline-offset-8{{end}}" href="/contact" data-drawer-link>Contact</a>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
{{template "site_start" .}}
|
||||
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
|
||||
{{define "contact.html"}}
|
||||
{{template "public_shell_start" .}}
|
||||
{{template "contact_content" .}}
|
||||
{{template "public_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "contact_partial.html"}}
|
||||
{{template "public_nav_oob" .}}
|
||||
{{template "contact_content" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "contact_content"}}
|
||||
<main id="main-content" class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
|
||||
<section class="grid gap-10 border-b border-neutral-200 pb-14 md:grid-cols-[0.85fr_1.15fr] md:items-end">
|
||||
<div>
|
||||
<p class="mb-3 text-sm uppercase tracking-[0.2em] text-neutral-500">Project enquiry</p>
|
||||
@ -87,4 +98,4 @@
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
{{template "site_end" .}}
|
||||
{{end}}
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
{{template "site_start" .}}
|
||||
<main>
|
||||
{{define "home.html"}}
|
||||
{{template "public_shell_start" .}}
|
||||
{{template "home_content" .}}
|
||||
{{template "public_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "home_partial.html"}}
|
||||
{{template "public_nav_oob" .}}
|
||||
{{template "home_content" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "home_content"}}
|
||||
<main id="main-content">
|
||||
<section class="relative min-h-[88vh] 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/40 via-black/10 to-black/65"></div>
|
||||
@ -7,7 +18,7 @@
|
||||
<p class="mb-4 max-w-xl text-sm uppercase tracking-[0.22em] text-white/75">{{.Content.Positioning}}</p>
|
||||
<h1 class="max-w-5xl text-5xl font-semibold leading-[0.95] md:text-8xl">{{.Content.HeroTitle}}</h1>
|
||||
<p class="mt-6 max-w-2xl text-lg leading-relaxed text-white/80 md:text-xl">{{.Content.HeroSubtitle}}</p>
|
||||
<div class="mt-8 flex flex-wrap gap-3" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
|
||||
<div class="mt-8 flex flex-wrap gap-3" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">
|
||||
<a href="{{.Content.HeroCTAURL}}" class="bg-white px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-neutral-950 hover:bg-neutral-200">{{.Content.HeroCTALabel}}</a>
|
||||
<a href="{{.Content.SecondaryCTAURL}}" class="border border-white/60 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-white hover:text-neutral-950">{{.Content.SecondaryCTALabel}}</a>
|
||||
</div>
|
||||
@ -18,7 +29,7 @@
|
||||
<h2 class="text-3xl font-semibold md:text-5xl">{{.Content.IntroTitle}}</h2>
|
||||
<div class="max-w-2xl">
|
||||
<p class="text-xl leading-relaxed text-neutral-600">{{.Content.IntroText}}</p>
|
||||
<a href="/about" class="mt-6 inline-flex text-sm uppercase tracking-[0.18em] text-neutral-500 hover:text-neutral-950" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Meet the studio</a>
|
||||
<a href="/about" class="mt-6 inline-flex text-sm uppercase tracking-[0.18em] text-neutral-500 hover:text-neutral-950" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Meet the studio</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -45,7 +56,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="px-5 py-20 md:px-8 md:py-28" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
|
||||
<section class="px-5 py-20 md:px-8 md:py-28" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="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>
|
||||
@ -103,8 +114,8 @@
|
||||
<p class="mb-3 text-sm uppercase tracking-[0.2em] text-neutral-500">{{.Content.AboutRole}}</p>
|
||||
<h2 class="text-4xl font-semibold md:text-6xl">{{.Content.AboutName}}</h2>
|
||||
<p class="mt-6 max-w-2xl text-xl leading-relaxed text-neutral-600">{{.Content.AboutBio}}</p>
|
||||
<a href="/about" class="mt-8 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Studio profile</a>
|
||||
<a href="/about" class="mt-8 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Studio profile</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{template "site_end" .}}
|
||||
{{end}}
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
{{template "site_start" .}}
|
||||
<main class="pt-28 md:pt-36">
|
||||
{{define "project.html"}}
|
||||
{{template "public_shell_start" .}}
|
||||
{{template "project_content" .}}
|
||||
{{template "public_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "project_partial.html"}}
|
||||
{{template "public_nav_oob" .}}
|
||||
{{template "project_content" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "project_content"}}
|
||||
<main id="main-content" 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}} · {{.Project.Status}}</p>
|
||||
<div class="grid gap-8 md:grid-cols-[1.2fr_0.8fr] md:items-end">
|
||||
@ -34,4 +45,4 @@
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{template "site_end" .}}
|
||||
{{end}}
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
{{template "site_start" .}}
|
||||
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
|
||||
{{define "projects.html"}}
|
||||
{{template "public_shell_start" .}}
|
||||
{{template "projects_content" .}}
|
||||
{{template "public_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "projects_partial.html"}}
|
||||
{{template "public_nav_oob" .}}
|
||||
{{template "projects_content" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "projects_content"}}
|
||||
<main id="main-content" 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>
|
||||
@ -7,7 +18,7 @@
|
||||
</div>
|
||||
<p class="max-w-md text-neutral-600">A visual index of architectural and interior design work.</p>
|
||||
</div>
|
||||
<div class="grid gap-x-5 gap-y-10 sm:grid-cols-2 lg:grid-cols-3" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
|
||||
<div class="grid gap-x-5 gap-y-10 sm:grid-cols-2 lg:grid-cols-3" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">
|
||||
{{range .Projects}}
|
||||
<a href="/projects/{{.Slug}}" class="group block">
|
||||
<div class="aspect-[4/3] overflow-hidden bg-neutral-200">
|
||||
@ -25,4 +36,4 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</main>
|
||||
{{template "site_end" .}}
|
||||
{{end}}
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
{{template "site_start" .}}
|
||||
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
|
||||
{{define "services.html"}}
|
||||
{{template "public_shell_start" .}}
|
||||
{{template "services_content" .}}
|
||||
{{template "public_shell_end" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "services_partial.html"}}
|
||||
{{template "public_nav_oob" .}}
|
||||
{{template "services_content" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "services_content"}}
|
||||
<main id="main-content" class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
|
||||
<section class="grid gap-10 border-b border-neutral-200 pb-14 md:grid-cols-[0.82fr_1.18fr] md:items-end">
|
||||
<div>
|
||||
<p class="mb-3 text-sm uppercase tracking-[0.2em] text-neutral-500">{{.Content.ServiceArea}}</p>
|
||||
@ -64,8 +75,8 @@
|
||||
<h2 class="text-3xl font-semibold">Ready to discuss a project?</h2>
|
||||
<div>
|
||||
<p class="max-w-2xl text-lg leading-relaxed text-neutral-600">Use the enquiry form to share the project type, site location, budget range, and timeline.</p>
|
||||
<a href="/contact" class="mt-6 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Start an enquiry</a>
|
||||
<a href="/contact" class="mt-6 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Start an enquiry</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{template "site_end" .}}
|
||||
{{end}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user