Full design · Marketing
Portfolio
The complete portfolio and studio back office for Quarter & Grain, the solo Tasmanian furniture practice of maker Asa Lindqvist.
Live preview. Click around, it works.
About this template
The complete portfolio and studio back office for Quarter & Grain, the solo Tasmanian furniture practice of maker Asa Lindqvist. An image led Home carries the work itself in an asymmetric grid that reveals each piece's title and year on hover. A Work index filters by discipline (Seating, Tables, Case Pieces, Architectural Joinery) and by year, and switches between a grid and a list. Every project opens as its own Case study, and no two share a layout: a full bleed hero, a sticky photograph beside a scrolling text column, a process or material strip built from typeset detail rather than a second photograph where a project has only one, credits naming the client, materials, dimensions and timeline, and a link to the next project. About carries the maker's portrait, a practice note, clients (marked Sample) and press (marked Sample). Contact is a working enquiry form: project type, budget range and timeline, all validated, ending in a demo submission confirmation. A visible Viewing as Owner control switches to the studio's own back office: a Projects editor that reorders work by drag and drop with a keyboard alternative, publishes or hides a project, and sets its cover from its own photographs, plus an Enquiries inbox with status, notes, reply templates and a CSV preview built from a Blob, never a real download.
What the agents are asked
The first message sent when you use this template. You can edit the plan before anything is built.
Adapt this furniture maker's portfolio to my own creative practice. Keep the image led Home grid with its hover reveal, the Work index with its discipline and year filters and grid/list toggle, the Case study pattern where each project gets its own layout rhythm (full bleed image, sticky image beside text, a process or material strip, credits, next project), the About page and the validated enquiry form on Contact, and the owner side (Projects editor with drag reordering, publish or hide, cover selection, and the Enquiries inbox with reply templates and CSV preview). Change the maker's name, discipline categories, projects, photographs, clients and press mentions to match my own work, keep clients, press and the CSV export clearly marked Sample or demo until I connect a real database and email service the way the README describes, and never put a secret key in the browser.
The portfolio and studio back office of Quarter & Grain, the one person workshop of Tasmanian furniture maker Asa Lindqvist, working solid native timber from Cygnet. The work carries the site: an image led Home, a filterable Work index, a Case study whose layout changes project to project, About and a validated Contact enquiry form. A "Viewing as: Owner" control reveals the studio's own back office, a Projects editor and an Enquiries inbox, sized to what a solo maker actually needs rather than a full CRM.
The maker, the studio, every client and every press mention are fictional and marked Sample where they appear. This template is not modelled on a real person, studio or publication.
Identity
- Colours: Linen
#F1EBDE, Ink#231F1B, Oiled timber#8B5A2B. Status colours on enquiries (new, in progress, quoted, booked, archived) are separate tokens, never the brand colours reused. Both a light "paper" theme and a dark "workshop at night" theme;--accentshifts to a lighter tint in dark mode so it still reads against a dark ground. - Type: Bitter (display, a slab serif for headings and the wordmark), Karla (text and every control).
- Motion: one staggered entrance on first load,
view()scroll reveals with an IntersectionObserver fallback (revealAll()also fires on a 1.2 second timeout and onvisibilitychange, so a hidden tab or a missed observer never leaves the page invisible), aclip-pathreveal on photographs, view transitions between screens, 150 to 200 ms hovers. The orchestrated moment, "the seam closes": on load, the hero photograph opens along a diagonal saw line, the Home grid's tiles drop into place largest to smallest like boards finding a bench, and the studio name planes on letter by letter. All of it behindprefers-reduced-motion, and the video hero tile never autoplays when it is set.
Art direction
One shoot: soft overcast coastal daylight through a large workshop window, cool neutral white balance around 5600K, even soft shadows, 50mm at f/2.8, fine film grain, muted warm timber tones against pale limewashed walls or a bare concrete floor, natural skin with no airbrushing on the portrait, no text, no logos, no maker's mark visible in any photograph.
Photo list
One photograph per project; the two projects with a second photograph never share it with another project, and the second photograph is used within that same project's own case study (a detail shot beside the hero, or the second half of a pair), never on another project's page.
| Name | Ratio | Project (own photo only) |
|---|---|---|
sawtooth-bench-hero | 16:9 | Sawtooth Bench (cover) |
sawtooth-bench-joint | 1:1 | Sawtooth Bench (detail, and the source of the hero video loop) |
kerf-chair-hero | 2:3 | Kerf Chair (cover) |
longboard-table-hero | 3:2 | Long Board Table (cover) |
longboard-table-grain | 4:5 | Long Board Table (detail) |
quarter-table-hero | 3:4 | Quarter Table (cover) |
ridge-sideboard-hero | 3:2 | Ridge Sideboard (cover) |
cygnet-cabinet-hero | 4:3 | Cygnet Cabinet (cover) |
stair-rail-hero | 2:1 | Stair & Rail (cover) |
asa-portrait | 1:1 | About (the maker's portrait) |
sawtooth-bench-joint-loop.mp4 is a five second, muted, looping hero video made from sawtooth-bench-joint, used as the largest tile on Home. It is aria-hidden, paused when the tab is hidden or the tile scrolls off screen, and never autoplays under prefers-reduced-motion (the poster image shows instead). Prompts are in media.json. Images are referenced as /template-media/portfolio/<name>.webp and the video as /template-media/portfolio/<name>.mp4.
Roles and demo mode
Every screen shares one demo: nothing to sign in to. A "Viewing as: Visitor / Owner" control in the strip at the top of every page switches between the public site and the studio's own back office (Studio: Projects, Studio: Enquiries), the same idea as a customer and owner side, sized to a one person practice. Switching back to Visitor while looking at a Studio screen returns to Home. The strip also shows how many sample enquiries are in the demo and a one step "Reset sample data" link.
SAMPLE_VERSION in script.js covers the sample enquiries: if a visitor's stored version differs, the four sample enquiries are replaced with a freshly dated set and any enquiry a visitor actually submitted through Contact is kept. Projects, clients, press and the studio note are the catalogue itself, identity data an owner edits from the Projects editor, so a version bump never overwrites a reorder, a hidden project or a changed cover photo.
Screens
- Home (
#/): an image led index that is the work itself. An asymmetric grid built from every published project's cover photo, largest tile first (the hero video loop), hovering or focusing a tile reveals its discipline, title and year over the image; on touch and small screens the caption shows without a hover step. - Work (
#/work): every project, filterable by discipline (chips) and year (a select), a text search across title, material and client, a Newest first / Oldest first sort, and a grid or list toggle that is remembered between visits. An empty state with "Clear filters" when nothing matches. - Case study (
#/work/<slug>): a full bleed hero on every project, then one of three rhythms depending on the project: a sticky photograph beside a scrolling text column (most projects, reusing the project's own photo a second time as the sticky image when it has only one); a side by side pair of the project's own two photographs captioned "Full length" and "Detail" for the one project shot as a matched pair (Long Board Table); or a typeset process strip of numbered steps and material swatches, with the project's second photograph inline where it has one (Sawtooth Bench, Ridge Sideboard). Every case study ends with credits (client, marked Sample where it is, materials, dimensions, timeline) and a link to the next project. - About (
#/about): the maker's portrait, a practice statement, quick facts (location, working since, practice areas), a Clients list and a Press list, both marked Sample. - Contact (
#/contact): an enquiry form (name, email, project type, budget range, timeline, message), validated on submit with inline errors and focus moved to the first problem field, ending in a demo submission confirmation that explains nothing was emailed. - Studio: Projects (
#/studio/projects, Owner only): every project including hidden ones, reordered by dragging the grip handle or, with the keyboard, focusing the grip, pressing Space to pick a row up, the arrow keys to move it and Space again to drop it (Escape cancels), publishing or hiding a project with an undo toast on hide, and choosing which of that project's own photos is the cover from a small picker. - Studio: Enquiries (
#/studio/enquiries, Owner only): every enquiry, filterable by status with live counts, a detail pane with the full message, an inline status change, private notes that save when you click away, four reply templates that compose a personalised draft (never sent), a Copy button, and "Preview CSV" which reads the export text back out of a generatedBlobinto an on page preview. There is no download link anywhere in this screen: exporting for real is described below.
Also: light and dark themes, a working mobile tab bar, empty and loading states, and every animation short circuited under prefers-reduced-motion.
Data model
store in script.js holds studio, clients, press, projects and enquiries, shaped exactly like the tables in schema.sql, with the same seed rows. enquiries is the only table a visitor writes to, through store.rpc.submitEnquiry(), which mirrors public.submit_enquiry(): it validates every field, refuses a second enquiry from the same email within ten minutes, and fixes status to new and is_sample to false so a script cannot forge a Sample row. projects.cover_photo is constrained (in schema.sql, and checked in store.setCover()) to be one of that project's own photos, so a cover can never point at a photograph the project does not own.
Moving from localStorage to Supabase
Replace store.load()'s localStorage read with Supabase queries that keep the same shape, so every render function is unchanged:
async function loadContent(supabase) {
const [{ data: studio }, { data: projects }, { data: clients }, { data: press }] = await Promise.all([
supabase.from('studio').select('id, name, maker_name, tagline, statement, location, founded_year, email, phone, portrait_name').single(),
supabase.from('projects').select('*').order('sort_order'),
supabase.from('clients').select('*').order('sort_order'),
supabase.from('press').select('*').order('sort_order'),
]);
// shape into the same `studio`, `projects`, `clients`, `press` the views already render
}store.rpc.submitEnquiry() becomes one call to the submit_enquiry function with the anon key. The browser never writes the enquiries table directly: the function trims and validates the input, refuses a repeat within ten minutes, and returns the new id.
async function submitEnquiry(supabase, input) {
const { data: id, error } = await supabase.rpc('submit_enquiry', {
_name: input.name, _email: input.email, _project_type: input.project_type,
_budget_range: input.budget_range, _timeline: input.timeline, _message: input.message,
});
if (error) throw error; // a repeat within ten minutes arrives here: show its message
return id;
}Server side functions you will need
- List and update enquiries for Studio: Enquiries:
schema.sqlgrantsanonnothing onenquiriesexcept callingsubmit_enquiry, so a leaked anon key can never read another visitor's enquiry. The owner reads and updates enquiries directly once signed in as the row referenced bystudio.owner_id, gated byis_owner(); no extra server function is needed for that part. - Notify the studio by email on a new enquiry: an Edge Function triggered by a database webhook on
insert into enquiries, emailing the studio's own inbox through your email service. The email key stays on the server, never in the browser. - Real sign in for the owner: wire the "Viewing as: Owner" control to Supabase Auth instead of a local preference once there is a real account;
studio.owner_idalready drives every owner only policy inschema.sql, so signing in as that user is the only change needed for the Projects editor and Enquiries inbox to become real. - A real CSV download: once there is a signed in owner, "Preview CSV" can sit next to a real "Export CSV" that builds the same
Bloband triggersURL.createObjectURLwith an anchor'sdownloadattribute, exactly as the preview already builds the text, just without the preview dialog in front of it.
What was not verified
Built and checked with tools/check-template.mjs. Motion and the hero video loop were authored against the tested reveal pattern used elsewhere in the catalogue (a timeout and a visibilitychange fallback so a hidden tab never leaves content invisible) but were not watched playing in a visible browser tab by this session; see the build report for what a reviewer should confirm on a real screen.