Pre-refactor checkpoint

This commit is contained in:
V
2026-05-17 13:36:50 +01:00
parent fac53d7b85
commit c4d199e20a
32 changed files with 1646 additions and 122 deletions
+24 -4
View File
@@ -2,13 +2,26 @@ package store
import "context"
func (s *Store) SaveContact(ctx context.Context, name, email, message string) error {
_, err := s.db.ExecContext(ctx, `insert into contact_requests (name, email, message) values (?, ?, ?)`, name, email, message)
func (s *Store) SaveContact(ctx context.Context, request ContactRequest) error {
_, err := s.db.ExecContext(ctx, `insert into contact_requests (
name, email, phone, project_type, project_location, budget_range, timeline, message, status, notes
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
request.Name,
request.Email,
request.Phone,
request.ProjectType,
request.ProjectLocation,
request.BudgetRange,
request.Timeline,
request.Message,
coalesceString(request.Status, "new"),
request.Notes,
)
return err
}
func (s *Store) ContactRequests(ctx context.Context) ([]ContactRequest, error) {
rows, err := s.db.QueryContext(ctx, `select id, name, email, message, created_at from contact_requests order by created_at desc, id desc`)
rows, err := s.db.QueryContext(ctx, `select id, name, email, phone, project_type, project_location, budget_range, timeline, message, status, notes, created_at from contact_requests order by created_at desc, id desc`)
if err != nil {
return nil, err
}
@@ -16,10 +29,17 @@ func (s *Store) ContactRequests(ctx context.Context) ([]ContactRequest, error) {
var requests []ContactRequest
for rows.Next() {
var r ContactRequest
if err := rows.Scan(&r.ID, &r.Name, &r.Email, &r.Message, &r.CreatedAt); err != nil {
if err := rows.Scan(&r.ID, &r.Name, &r.Email, &r.Phone, &r.ProjectType, &r.ProjectLocation, &r.BudgetRange, &r.Timeline, &r.Message, &r.Status, &r.Notes, &r.CreatedAt); err != nil {
return nil, err
}
requests = append(requests, r)
}
return requests, rows.Err()
}
func coalesceString(value, fallback string) string {
if value == "" {
return fallback
}
return value
}
+192 -5
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
)
@@ -14,11 +15,32 @@ func (s *Store) Migrate(adminUsername, adminPassword string) error {
id integer primary key check (id = 1),
hero_title text not null,
hero_subtitle text not null,
positioning text not null default '',
hero_cta_label text not null default '',
hero_cta_url text not null default '',
secondary_cta_label text not null default '',
secondary_cta_url text not null default '',
intro_title text not null,
intro_text text not null,
service_one_title text not null default '',
service_one_text text not null default '',
service_two_title text not null default '',
service_two_text text not null default '',
service_three_title text not null default '',
service_three_text text not null default '',
process_one_title text not null default '',
process_one_text text not null default '',
process_two_title text not null default '',
process_two_text text not null default '',
process_three_title text not null default '',
process_three_text text not null default '',
about_name text not null,
about_role text not null,
about_bio text not null,
studio_philosophy text not null default '',
studio_approach text not null default '',
studio_credentials text not null default '',
service_area text not null default '',
email text not null,
phone text not null,
location text not null,
@@ -32,6 +54,10 @@ func (s *Store) Migrate(adminUsername, adminPassword string) error {
location text not null,
year text not null,
category text not null,
summary text not null default '',
scope text not null default '',
status text not null default '',
position integer not null default 0,
description text not null,
cover_image text not null,
featured integer not null default 0,
@@ -44,6 +70,23 @@ func (s *Store) Migrate(adminUsername, adminPassword string) error {
caption text not null,
position integer not null default 0
)`,
`create table if not exists services (
id integer primary key autoincrement,
title text not null,
summary text not null,
details text not null,
position integer not null default 0,
active integer not null default 1,
created_at datetime not null default current_timestamp
)`,
`create table if not exists faqs (
id integer primary key autoincrement,
question text not null,
answer text not null,
position integer not null default 0,
active integer not null default 1,
created_at datetime not null default current_timestamp
)`,
`create table if not exists contact_requests (
id integer primary key autoincrement,
name text not null,
@@ -67,9 +110,95 @@ func (s *Store) Migrate(adminUsername, adminPassword string) error {
return err
}
}
siteColumns := map[string]string{
"positioning": "text not null default 'Residential architecture and interiors in London'",
"hero_cta_label": "text not null default 'Start an enquiry'",
"hero_cta_url": "text not null default '/contact'",
"secondary_cta_label": "text not null default 'View projects'",
"secondary_cta_url": "text not null default '/projects'",
"service_one_title": "text not null default 'Residential architecture'",
"service_one_text": "text not null default 'Carefully planned homes, extensions, and spatial changes shaped around daily life.'",
"service_two_title": "text not null default 'Interior architecture'",
"service_two_text": "text not null default 'Layouts, materials, storage, lighting, and built-in elements considered as one whole.'",
"service_three_title": "text not null default 'Early consultation'",
"service_three_text": "text not null default 'Focused advice for feasibility, priorities, budgets, and the next practical steps.'",
"process_one_title": "text not null default 'Listen'",
"process_one_text": "text not null default 'Clarify the site, constraints, ambitions, and what the project needs to solve.'",
"process_two_title": "text not null default 'Shape'",
"process_two_text": "text not null default 'Develop a spatial direction through sketches, references, plans, and material thinking.'",
"process_three_title": "text not null default 'Refine'",
"process_three_text": "text not null default 'Coordinate details, decisions, and documentation so the work can move forward clearly.'",
"studio_philosophy": "text not null default 'The studio favours calm, durable spaces where proportion, daylight, materials, and storage do practical work without visual noise.'",
"studio_approach": "text not null default 'Projects begin with listening and careful briefing, then move through measured options, clear priorities, and detailed decisions at a pace suited to the client and site.'",
"studio_credentials": "text not null default 'Independent architecture and interior design practice working across residential projects, renovations, and compact cultural spaces.'",
"service_area": "text not null default 'London and selected UK projects'",
}
for column, definition := range siteColumns {
if err := s.ensureColumn("site_content", column, definition); err != nil {
return err
}
}
projectColumns := map[string]string{
"summary": "text not null default ''",
"scope": "text not null default ''",
"status": "text not null default 'Completed'",
"position": "integer not null default 0",
}
for column, definition := range projectColumns {
if err := s.ensureColumn("projects", column, definition); err != nil {
return err
}
}
if err := s.ensureColumn("contact_requests", "phone", "text not null default ''"); err != nil {
return err
}
if err := s.ensureColumn("contact_requests", "project_type", "text not null default ''"); err != nil {
return err
}
if err := s.ensureColumn("contact_requests", "project_location", "text not null default ''"); err != nil {
return err
}
if err := s.ensureColumn("contact_requests", "budget_range", "text not null default ''"); err != nil {
return err
}
if err := s.ensureColumn("contact_requests", "timeline", "text not null default ''"); err != nil {
return err
}
if err := s.ensureColumn("contact_requests", "status", "text not null default 'new'"); err != nil {
return err
}
if err := s.ensureColumn("contact_requests", "notes", "text not null default ''"); err != nil {
return err
}
return s.seed(adminUsername, adminPassword)
}
func (s *Store) ensureColumn(table, column, definition string) error {
rows, err := s.db.Query(fmt.Sprintf("pragma table_info(%s)", table))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var cid int
var name, typ string
var notNull int
var defaultValue sql.NullString
var pk int
if err := rows.Scan(&cid, &name, &typ, &notNull, &defaultValue, &pk); err != nil {
return err
}
if name == column {
return nil
}
}
if err := rows.Err(); err != nil {
return err
}
_, err = s.db.Exec(fmt.Sprintf("alter table %s add column %s %s", table, column, definition))
return err
}
func (s *Store) seed(adminUsername, adminPassword string) error {
var count int
if err := s.db.QueryRow(`select count(*) from site_content`).Scan(&count); err != nil {
@@ -77,16 +206,41 @@ func (s *Store) seed(adminUsername, adminPassword string) error {
}
if count == 0 {
_, err := s.db.Exec(`insert into site_content (
id, hero_title, hero_subtitle, intro_title, intro_text, about_name, about_role, about_bio,
id, hero_title, hero_subtitle, positioning, hero_cta_label, hero_cta_url, secondary_cta_label, secondary_cta_url,
intro_title, intro_text,
service_one_title, service_one_text, service_two_title, service_two_text, service_three_title, service_three_text,
process_one_title, process_one_text, process_two_title, process_two_text, process_three_title, process_three_text,
about_name, about_role, about_bio, studio_philosophy, studio_approach, studio_credentials, service_area,
email, phone, location, hero_image, about_image
) values (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) values (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
"Archi Folio",
"Spatial design, architecture, and interiors shaped through quiet detail.",
"Residential architecture and interiors in London",
"Start an enquiry",
"/contact",
"View projects",
"/projects",
"Selected residential and cultural spaces",
"A compact portfolio for showing image-led architectural work, interior concepts, and project narratives.",
"Residential architecture",
"Carefully planned homes, extensions, and spatial changes shaped around daily life.",
"Interior architecture",
"Layouts, materials, storage, lighting, and built-in elements considered as one whole.",
"Early consultation",
"Focused advice for feasibility, priorities, budgets, and the next practical steps.",
"Listen",
"Clarify the site, constraints, ambitions, and what the project needs to solve.",
"Shape",
"Develop a spatial direction through sketches, references, plans, and material thinking.",
"Refine",
"Coordinate details, decisions, and documentation so the work can move forward clearly.",
"Alex Morgan",
"Architect & Interior Designer",
"I design calm, functional spaces with attention to proportion, material, light, and the daily rituals of the people who use them.",
"The studio favours calm, durable spaces where proportion, daylight, materials, and storage do practical work without visual noise.",
"Projects begin with listening and careful briefing, then move through measured options, clear priorities, and detailed decisions at a pace suited to the client and site.",
"Independent architecture and interior design practice working across residential projects, renovations, and compact cultural spaces.",
"London and selected UK projects",
"studio@example.com",
"+44 20 0000 0000",
"London, United Kingdom",
@@ -103,9 +257,9 @@ func (s *Store) seed(adminUsername, adminPassword string) error {
}
if count == 0 {
projects := []Project{
{Slug: "courtyard-house", Title: "Courtyard House", Location: "Bath, UK", Year: "2025", Category: "Residential", Description: "A private house organized around a quiet internal garden, using warm timber, stone, and filtered daylight.", CoverImage: "/static/placeholders/project-1.svg", Featured: true},
{Slug: "atelier-apartment", Title: "Atelier Apartment", Location: "London, UK", Year: "2024", Category: "Interior", Description: "A compact apartment refit with integrated storage, gallery-like surfaces, and a flexible work area.", CoverImage: "/static/placeholders/project-2.svg", Featured: true},
{Slug: "gallery-room", Title: "Gallery Room", Location: "Amsterdam, NL", Year: "2024", Category: "Cultural", Description: "A small exhibition environment designed for shifting light levels, sculpture, and intimate events.", CoverImage: "/static/placeholders/project-3.svg", Featured: true},
{Slug: "courtyard-house", Title: "Courtyard House", Location: "Bath, UK", Year: "2025", Category: "Residential", Summary: "A private house arranged around a quiet internal garden.", Scope: "Architecture, interiors, material strategy", Status: "Completed", Position: 1, Description: "A private house organized around a quiet internal garden, using warm timber, stone, and filtered daylight.", CoverImage: "/static/placeholders/project-1.svg", Featured: true},
{Slug: "atelier-apartment", Title: "Atelier Apartment", Location: "London, UK", Year: "2024", Category: "Interior", Summary: "A compact apartment refit with integrated storage and a flexible work area.", Scope: "Interior architecture, joinery, lighting", Status: "Completed", Position: 2, Description: "A compact apartment refit with integrated storage, gallery-like surfaces, and a flexible work area.", CoverImage: "/static/placeholders/project-2.svg", Featured: true},
{Slug: "gallery-room", Title: "Gallery Room", Location: "Amsterdam, NL", Year: "2024", Category: "Cultural", Summary: "A small exhibition environment for sculpture, events, and shifting light.", Scope: "Spatial design, exhibition planning", Status: "Concept", Position: 3, Description: "A small exhibition environment designed for shifting light levels, sculpture, and intimate events.", CoverImage: "/static/placeholders/project-3.svg", Featured: true},
}
for _, p := range projects {
id, err := s.CreateProject(context.Background(), p)
@@ -120,6 +274,39 @@ func (s *Store) seed(adminUsername, adminPassword string) error {
}
}
if err := s.db.QueryRow(`select count(*) from services`).Scan(&count); err != nil {
return err
}
if count == 0 {
services := []Service{
{Title: "Residential architecture", Summary: "New homes, extensions, and spatial reconfiguration for private clients.", Details: "Suitable for homeowners who need a clear architectural direction, measured priorities, planning support, and coordinated design decisions from early brief through detailed development.", Position: 1, Active: true},
{Title: "Renovation and extensions", Summary: "Careful upgrades to existing homes where light, storage, and flow need to work harder.", Details: "Useful for period properties, compact urban homes, and phased refurbishments where the existing building needs to be understood before design moves are made.", Position: 2, Active: true},
{Title: "Interior architecture", Summary: "Layouts, joinery, materials, lighting, and finishes developed as one spatial system.", Details: "For clients who need the interior to feel resolved rather than decorated, with attention to proportion, thresholds, storage, and daily use.", Position: 3, Active: true},
{Title: "Early-stage consultation", Summary: "Focused advice before committing to a larger scope of work.", Details: "A practical starting point for feasibility, budget alignment, project priorities, or deciding whether a property or idea has the right potential.", Position: 4, Active: true},
}
for _, service := range services {
if err := s.CreateService(context.Background(), service); err != nil {
return err
}
}
}
if err := s.db.QueryRow(`select count(*) from faqs`).Scan(&count); err != nil {
return err
}
if count == 0 {
faqs := []FAQ{
{Question: "What size projects are a good fit?", Answer: "The studio is best suited to residential projects, renovations, compact interiors, and early-stage design work where careful spatial thinking is valued.", Position: 1, Active: true},
{Question: "Where does the studio work?", Answer: "The studio is based in London and considers selected projects across the UK depending on scope, timing, and site needs.", Position: 2, Active: true},
{Question: "Can I book a consultation before a full project?", Answer: "Yes. Early consultation is useful for clarifying feasibility, budget, priorities, and whether a larger design process is appropriate.", Position: 3, Active: true},
}
for _, faq := range faqs {
if err := s.CreateFAQ(context.Background(), faq); err != nil {
return err
}
}
}
hash, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
return err
+93
View File
@@ -31,3 +31,96 @@ func TestMigrateUpdatesAdminCredentials(t *testing.T) {
t.Fatalf("expected old username to be removed, got %v", err)
}
}
func TestContactRequestsSupportQualificationFields(t *testing.T) {
st, err := Open(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
if err := st.Migrate("admin", "password"); err != nil {
t.Fatal(err)
}
err = st.SaveContact(t.Context(), ContactRequest{
Name: "Jane",
Email: "jane@example.com",
Phone: "123",
ProjectType: "Renovation",
ProjectLocation: "London",
BudgetRange: "GBP 250k-500k",
Timeline: "3-6 months",
Message: "Project notes",
})
if err != nil {
t.Fatal(err)
}
requests, err := st.ContactRequests(t.Context())
if err != nil {
t.Fatal(err)
}
if len(requests) != 1 || requests[0].ProjectLocation != "London" || requests[0].Status != "new" {
t.Fatalf("unexpected request fields: %+v", requests)
}
}
func TestSiteContentIncludesPhaseTwoFields(t *testing.T) {
st, err := Open(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
if err := st.Migrate("admin", "password"); err != nil {
t.Fatal(err)
}
content, err := st.SiteContent(t.Context())
if err != nil {
t.Fatal(err)
}
if content.Positioning == "" || content.HeroCTALabel == "" || content.ServiceOneTitle == "" || content.StudioPhilosophy == "" {
t.Fatalf("expected seeded phase two content, got %+v", content)
}
}
func TestServicesAndFAQsAreSeeded(t *testing.T) {
st, err := Open(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
if err := st.Migrate("admin", "password"); err != nil {
t.Fatal(err)
}
services, err := st.Services(t.Context(), true)
if err != nil {
t.Fatal(err)
}
faqs, err := st.FAQs(t.Context(), true)
if err != nil {
t.Fatal(err)
}
if len(services) < 4 || len(faqs) < 3 {
t.Fatalf("expected seeded services and FAQs, got services=%d faqs=%d", len(services), len(faqs))
}
}
func TestSeededProjectsIncludeDepthFields(t *testing.T) {
st, err := Open(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
if err := st.Migrate("admin", "password"); err != nil {
t.Fatal(err)
}
project, err := st.ProjectBySlug(t.Context(), "courtyard-house")
if err != nil {
t.Fatal(err)
}
if project.Summary == "" || project.Scope == "" || project.Status == "" || project.Position == 0 {
t.Fatalf("expected project depth fields, got %+v", project)
}
}
+9 -9
View File
@@ -6,11 +6,11 @@ import (
)
func (s *Store) Projects(ctx context.Context, featuredOnly bool) ([]Project, error) {
query := `select id, slug, title, location, year, category, description, cover_image, featured, created_at from projects`
query := `select id, slug, title, location, year, category, summary, scope, status, position, description, cover_image, featured, created_at from projects`
if featuredOnly {
query += ` where featured = 1`
}
query += ` order by created_at desc, id desc`
query += ` order by position asc, created_at desc, id desc`
rows, err := s.db.QueryContext(ctx, query)
if err != nil {
return nil, err
@@ -20,7 +20,7 @@ func (s *Store) Projects(ctx context.Context, featuredOnly bool) ([]Project, err
for rows.Next() {
var p Project
var featured int
if err := rows.Scan(&p.ID, &p.Slug, &p.Title, &p.Location, &p.Year, &p.Category, &p.Description, &p.CoverImage, &featured, &p.CreatedAt); err != nil {
if err := rows.Scan(&p.ID, &p.Slug, &p.Title, &p.Location, &p.Year, &p.Category, &p.Summary, &p.Scope, &p.Status, &p.Position, &p.Description, &p.CoverImage, &featured, &p.CreatedAt); err != nil {
return nil, err
}
p.Featured = featured == 1
@@ -32,8 +32,8 @@ func (s *Store) Projects(ctx context.Context, featuredOnly bool) ([]Project, err
func (s *Store) ProjectBySlug(ctx context.Context, slug string) (Project, error) {
var p Project
var featured int
err := s.db.QueryRowContext(ctx, `select id, slug, title, location, year, category, description, cover_image, featured, created_at from projects where slug = ?`, slug).
Scan(&p.ID, &p.Slug, &p.Title, &p.Location, &p.Year, &p.Category, &p.Description, &p.CoverImage, &featured, &p.CreatedAt)
err := s.db.QueryRowContext(ctx, `select id, slug, title, location, year, category, summary, scope, status, position, description, cover_image, featured, created_at from projects where slug = ?`, slug).
Scan(&p.ID, &p.Slug, &p.Title, &p.Location, &p.Year, &p.Category, &p.Summary, &p.Scope, &p.Status, &p.Position, &p.Description, &p.CoverImage, &featured, &p.CreatedAt)
if err != nil {
return p, err
}
@@ -90,8 +90,8 @@ func (s *Store) ProjectImageForSlug(ctx context.Context, slug string, imageID in
}
func (s *Store) CreateProject(ctx context.Context, p Project) (int64, error) {
res, err := s.db.ExecContext(ctx, `insert into projects (slug, title, location, year, category, description, cover_image, featured) values (?, ?, ?, ?, ?, ?, ?, ?)`,
p.Slug, p.Title, p.Location, p.Year, p.Category, p.Description, p.CoverImage, boolInt(p.Featured))
res, err := s.db.ExecContext(ctx, `insert into projects (slug, title, location, year, category, summary, scope, status, position, description, cover_image, featured) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
p.Slug, p.Title, p.Location, p.Year, p.Category, p.Summary, p.Scope, p.Status, p.Position, p.Description, p.CoverImage, boolInt(p.Featured))
if err != nil {
return 0, err
}
@@ -99,8 +99,8 @@ func (s *Store) CreateProject(ctx context.Context, p Project) (int64, error) {
}
func (s *Store) UpdateProject(ctx context.Context, p Project) error {
_, err := s.db.ExecContext(ctx, `update projects set slug=?, title=?, location=?, year=?, category=?, description=?, cover_image=?, featured=? where id=?`,
p.Slug, p.Title, p.Location, p.Year, p.Category, p.Description, p.CoverImage, boolInt(p.Featured), p.ID)
_, err := s.db.ExecContext(ctx, `update projects set slug=?, title=?, location=?, year=?, category=?, summary=?, scope=?, status=?, position=?, description=?, cover_image=?, featured=? where id=?`,
p.Slug, p.Title, p.Location, p.Year, p.Category, p.Summary, p.Scope, p.Status, p.Position, p.Description, p.CoverImage, boolInt(p.Featured), p.ID)
return err
}
+85
View File
@@ -0,0 +1,85 @@
package store
import "context"
func (s *Store) Services(ctx context.Context, activeOnly bool) ([]Service, error) {
query := `select id, title, summary, details, position, active, created_at from services`
if activeOnly {
query += ` where active = 1`
}
query += ` order by position asc, id asc`
rows, err := s.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var services []Service
for rows.Next() {
var service Service
var active int
if err := rows.Scan(&service.ID, &service.Title, &service.Summary, &service.Details, &service.Position, &active, &service.CreatedAt); err != nil {
return nil, err
}
service.Active = active == 1
services = append(services, service)
}
return services, rows.Err()
}
func (s *Store) CreateService(ctx context.Context, service Service) error {
_, err := s.db.ExecContext(ctx, `insert into services (title, summary, details, position, active) values (?, ?, ?, ?, ?)`,
service.Title, service.Summary, service.Details, service.Position, boolInt(service.Active))
return err
}
func (s *Store) UpdateService(ctx context.Context, service Service) error {
_, err := s.db.ExecContext(ctx, `update services set title=?, summary=?, details=?, position=?, active=? where id=?`,
service.Title, service.Summary, service.Details, service.Position, boolInt(service.Active), service.ID)
return err
}
func (s *Store) DeleteService(ctx context.Context, id int64) error {
_, err := s.db.ExecContext(ctx, `delete from services where id = ?`, id)
return err
}
func (s *Store) FAQs(ctx context.Context, activeOnly bool) ([]FAQ, error) {
query := `select id, question, answer, position, active, created_at from faqs`
if activeOnly {
query += ` where active = 1`
}
query += ` order by position asc, id asc`
rows, err := s.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var faqs []FAQ
for rows.Next() {
var faq FAQ
var active int
if err := rows.Scan(&faq.ID, &faq.Question, &faq.Answer, &faq.Position, &active, &faq.CreatedAt); err != nil {
return nil, err
}
faq.Active = active == 1
faqs = append(faqs, faq)
}
return faqs, rows.Err()
}
func (s *Store) CreateFAQ(ctx context.Context, faq FAQ) error {
_, err := s.db.ExecContext(ctx, `insert into faqs (question, answer, position, active) values (?, ?, ?, ?)`,
faq.Question, faq.Answer, faq.Position, boolInt(faq.Active))
return err
}
func (s *Store) UpdateFAQ(ctx context.Context, faq FAQ) error {
_, err := s.db.ExecContext(ctx, `update faqs set question=?, answer=?, position=?, active=? where id=?`,
faq.Question, faq.Answer, faq.Position, boolInt(faq.Active), faq.ID)
return err
}
func (s *Store) DeleteFAQ(ctx context.Context, id int64) error {
_, err := s.db.ExecContext(ctx, `delete from faqs where id = ?`, id)
return err
}
+30 -4
View File
@@ -4,13 +4,39 @@ import "context"
func (s *Store) SiteContent(ctx context.Context) (SiteContent, error) {
var c SiteContent
err := s.db.QueryRowContext(ctx, `select hero_title, hero_subtitle, intro_title, intro_text, about_name, about_role, about_bio, email, phone, location, hero_image, about_image from site_content where id = 1`).
Scan(&c.HeroTitle, &c.HeroSubtitle, &c.IntroTitle, &c.IntroText, &c.AboutName, &c.AboutRole, &c.AboutBio, &c.Email, &c.Phone, &c.Location, &c.HeroImage, &c.AboutImage)
err := s.db.QueryRowContext(ctx, `select
hero_title, hero_subtitle, positioning, hero_cta_label, hero_cta_url, secondary_cta_label, secondary_cta_url,
intro_title, intro_text,
service_one_title, service_one_text, service_two_title, service_two_text, service_three_title, service_three_text,
process_one_title, process_one_text, process_two_title, process_two_text, process_three_title, process_three_text,
about_name, about_role, about_bio, studio_philosophy, studio_approach, studio_credentials, service_area,
email, phone, location, hero_image, about_image
from site_content where id = 1`).
Scan(
&c.HeroTitle, &c.HeroSubtitle, &c.Positioning, &c.HeroCTALabel, &c.HeroCTAURL, &c.SecondaryCTALabel, &c.SecondaryCTAURL,
&c.IntroTitle, &c.IntroText,
&c.ServiceOneTitle, &c.ServiceOneText, &c.ServiceTwoTitle, &c.ServiceTwoText, &c.ServiceThreeTitle, &c.ServiceThreeText,
&c.ProcessOneTitle, &c.ProcessOneText, &c.ProcessTwoTitle, &c.ProcessTwoText, &c.ProcessThreeTitle, &c.ProcessThreeText,
&c.AboutName, &c.AboutRole, &c.AboutBio, &c.StudioPhilosophy, &c.StudioApproach, &c.StudioCredentials, &c.ServiceArea,
&c.Email, &c.Phone, &c.Location, &c.HeroImage, &c.AboutImage,
)
return c, err
}
func (s *Store) UpdateSiteContent(ctx context.Context, c SiteContent) error {
_, err := s.db.ExecContext(ctx, `update site_content set hero_title=?, hero_subtitle=?, intro_title=?, intro_text=?, about_name=?, about_role=?, about_bio=?, email=?, phone=?, location=?, hero_image=?, about_image=? where id=1`,
c.HeroTitle, c.HeroSubtitle, c.IntroTitle, c.IntroText, c.AboutName, c.AboutRole, c.AboutBio, c.Email, c.Phone, c.Location, c.HeroImage, c.AboutImage)
_, err := s.db.ExecContext(ctx, `update site_content set
hero_title=?, hero_subtitle=?, positioning=?, hero_cta_label=?, hero_cta_url=?, secondary_cta_label=?, secondary_cta_url=?,
intro_title=?, intro_text=?,
service_one_title=?, service_one_text=?, service_two_title=?, service_two_text=?, service_three_title=?, service_three_text=?,
process_one_title=?, process_one_text=?, process_two_title=?, process_two_text=?, process_three_title=?, process_three_text=?,
about_name=?, about_role=?, about_bio=?, studio_philosophy=?, studio_approach=?, studio_credentials=?, service_area=?,
email=?, phone=?, location=?, hero_image=?, about_image=?
where id=1`,
c.HeroTitle, c.HeroSubtitle, c.Positioning, c.HeroCTALabel, c.HeroCTAURL, c.SecondaryCTALabel, c.SecondaryCTAURL,
c.IntroTitle, c.IntroText,
c.ServiceOneTitle, c.ServiceOneText, c.ServiceTwoTitle, c.ServiceTwoText, c.ServiceThreeTitle, c.ServiceThreeText,
c.ProcessOneTitle, c.ProcessOneText, c.ProcessTwoTitle, c.ProcessTwoText, c.ProcessThreeTitle, c.ProcessThreeText,
c.AboutName, c.AboutRole, c.AboutBio, c.StudioPhilosophy, c.StudioApproach, c.StudioCredentials, c.ServiceArea,
c.Email, c.Phone, c.Location, c.HeroImage, c.AboutImage)
return err
}
+72 -16
View File
@@ -1,6 +1,7 @@
package store
import (
"context"
"database/sql"
"os"
"path/filepath"
@@ -14,18 +15,39 @@ type Store struct {
}
type SiteContent struct {
HeroTitle string
HeroSubtitle string
IntroTitle string
IntroText string
AboutName string
AboutRole string
AboutBio string
Email string
Phone string
Location string
HeroImage string
AboutImage string
HeroTitle string
HeroSubtitle string
Positioning string
HeroCTALabel string
HeroCTAURL string
SecondaryCTALabel string
SecondaryCTAURL string
IntroTitle string
IntroText string
ServiceOneTitle string
ServiceOneText string
ServiceTwoTitle string
ServiceTwoText string
ServiceThreeTitle string
ServiceThreeText string
ProcessOneTitle string
ProcessOneText string
ProcessTwoTitle string
ProcessTwoText string
ProcessThreeTitle string
ProcessThreeText string
AboutName string
AboutRole string
AboutBio string
StudioPhilosophy string
StudioApproach string
StudioCredentials string
ServiceArea string
Email string
Phone string
Location string
HeroImage string
AboutImage string
}
type Project struct {
@@ -35,6 +57,10 @@ type Project struct {
Location string
Year string
Category string
Summary string
Scope string
Status string
Position int
Description string
CoverImage string
Featured bool
@@ -50,14 +76,40 @@ type ProjectImage struct {
Position int
}
type ContactRequest struct {
type Service struct {
ID int64
Name string
Email string
Message string
Title string
Summary string
Details string
Position int
Active bool
CreatedAt time.Time
}
type FAQ struct {
ID int64
Question string
Answer string
Position int
Active bool
CreatedAt time.Time
}
type ContactRequest struct {
ID int64
Name string
Email string
Phone string
ProjectType string
ProjectLocation string
BudgetRange string
Timeline string
Message string
Status string
Notes string
CreatedAt time.Time
}
type AdminUser struct {
ID int64
Username string
@@ -79,3 +131,7 @@ func Open(path string) (*Store, error) {
func (s *Store) Close() error {
return s.db.Close()
}
func (s *Store) Ping(ctx context.Context) error {
return s.db.PingContext(ctx)
}