This commit is contained in:
tumillanino
2025-10-28 12:36:37 +11:00
parent 186d6768fb
commit dad3ec655f
31 changed files with 2256 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
package handlers
import (
"database/sql"
"encoding/json"
"html/template"
"net/http"
"strconv"
"time"
"decor-by-hannahs/internal/auth"
"decor-by-hannahs/internal/db"
)
func BookingPageHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type data struct {
Title string
ActivePage string
}
_ = tmpl.ExecuteTemplate(w, "booking.tmpl", data{Title: "Book a Service", ActivePage: "booking"})
}
}
func BookingAPIHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method is not allowed", http.StatusMethodNotAllowed)
return
}
user, err := auth.GetUser(r.Context())
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
var in struct {
ServiceID string `json:"service_id"`
EventDate string `json:"event_date"`
Address string `json:"address"`
Notes string `json:"notes"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
eventTime, err := time.Parse(time.RFC3339, in.EventDate)
if err != nil {
http.Error(w, "Invalid date", http.StatusBadRequest)
return
}
svc, err := strconv.ParseInt(in.ServiceID, 10, 64)
if err != nil {
http.Error(w, "Invalid Service ID", http.StatusBadRequest)
return
}
svcID := sql.NullInt64{Int64: svc, Valid: true}
userID := sql.NullInt64{Int64: user.ID, Valid: true}
booking, err := q.CreateBooking(r.Context(), db.CreateBookingParams{
UserID: userID,
ServiceID: svcID,
EventDate: eventTime,
Address: sqlNullString(in.Address),
Notes: sqlNullString(in.Notes),
Status: sql.NullString{String: "pending", Valid: true},
})
if err != nil {
http.Error(w, "Failed to create booking", http.StatusInternalServerError)
return
}
writeJSON(w, booking)
}
}

View File

@@ -0,0 +1,25 @@
package handlers
import (
"html/template"
"net/http"
"decor-by-hannahs/internal/db"
)
func CatalogHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
svcs, err := q.ListServices(r.Context())
if err != nil {
http.Error(w, "unable to load services", http.StatusInternalServerError)
return
}
type data struct {
Services []db.Service
ActivePage string
}
if err := tmpl.ExecuteTemplate(w, "catalog.tmpl", data{Services: svcs, ActivePage: "catalog"}); err != nil {
http.Error(w, "Failed to render page", http.StatusInternalServerError)
}
}
}

View File

@@ -0,0 +1,50 @@
package handlers
import (
"html/template"
"net/http"
"os"
"path/filepath"
"sort"
"decor-by-hannahs/internal/db"
)
type Photo struct {
URL string
Caption string
}
func GalleryHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var photos []Photo
galleryDir := "assets/gallery-photos"
entries, err := os.ReadDir(galleryDir)
if err == nil {
for _, entry := range entries {
if !entry.IsDir() {
ext := filepath.Ext(entry.Name())
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" {
photos = append(photos, Photo{
URL: "/assets/gallery-photos/" + entry.Name(),
Caption: "",
})
}
}
}
}
sort.Slice(photos, func(i, j int) bool {
return photos[i].URL < photos[j].URL
})
type data struct {
Photos []Photo
ActivePage string
}
if err := tmpl.ExecuteTemplate(w, "gallery.tmpl", data{Photos: photos, ActivePage: "gallery"}); err != nil {
http.Error(w, "Failed to render page", http.StatusInternalServerError)
}
}
}

View File

@@ -0,0 +1,26 @@
package handlers
import (
"database/sql"
"encoding/json"
"net/http"
)
func sqlNullString(s string) sql.NullString {
if s == "" {
return sql.NullString{}
}
return sql.NullString{String: s, Valid: true}
}
func sqlNullUUID(s string) sql.NullString {
if s == "" {
return sql.NullString{}
}
return sql.NullString{String: s, Valid: true}
}
func writeJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}

54
internal/handlers/home.go Normal file
View File

@@ -0,0 +1,54 @@
package handlers
import (
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"decor-by-hannahs/internal/db"
)
func HomeHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("HomeHandler called: method=%s path=%s", r.Method, r.URL.Path)
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
var photos []Photo
carouselDir := "assets/carousel-photos"
entries, err := os.ReadDir(carouselDir)
if err == nil {
for _, entry := range entries {
if !entry.IsDir() {
ext := filepath.Ext(entry.Name())
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" {
photos = append(photos, Photo{
URL: "/assets/carousel-photos/" + entry.Name(),
Caption: "",
})
}
}
}
}
sort.Slice(photos, func(i, j int) bool {
return photos[i].URL < photos[j].URL
})
type data struct {
Title string
Photos []Photo
ActivePage string
}
if err := tmpl.ExecuteTemplate(w, "home.tmpl", data{Title: "Home", Photos: photos, ActivePage: "home"}); err != nil {
log.Printf("Error executing home.tmpl: %v", err)
http.Error(w, "Failed to render page", http.StatusInternalServerError)
}
}
}

View File

@@ -0,0 +1,59 @@
package handlers
import (
"html/template"
"net/http"
)
type OryRedirectData struct {
Title string
ActivePage string
Flow string
Message string
Type string
}
func OryRedirectHandler(tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
flow := r.URL.Query().Get("flow")
var message string
var msgType string
switch flow {
case "login":
message = "Successfully logged in!"
msgType = "success"
case "registration":
message = "Account created successfully!"
msgType = "success"
case "settings":
message = "Settings updated successfully!"
msgType = "success"
case "verification":
message = "Email verified successfully!"
msgType = "success"
case "recovery":
message = "Password recovered successfully!"
msgType = "success"
case "logout":
message = "Successfully logged out!"
msgType = "info"
default:
message = "Action completed successfully!"
msgType = "success"
}
data := OryRedirectData{
Title: "Redirect",
ActivePage: "",
Flow: flow,
Message: message,
Type: msgType,
}
if err := tmpl.ExecuteTemplate(w, "ory_redirect.tmpl", data); err != nil {
http.Error(w, "Failed to render page", http.StatusInternalServerError)
}
}
}

View File

@@ -0,0 +1,82 @@
package handlers
import (
"html/template"
"log"
"net/http"
"os"
"strconv"
"decor-by-hannahs/internal/auth"
"decor-by-hannahs/internal/db"
)
type BookingWithService struct {
ID int64
ServiceName string
ServiceDescription string
ServicePriceCents int32
EventDate string
Address string
Notes string
Status string
CreatedAt string
}
type ProfileData struct {
Authenticated bool
Email string
DisplayName string
ID string
ActivePage string
Bookings []BookingWithService
OrySettingsURL string
}
func ProfileHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
pd := ProfileData{ActivePage: "profile"}
user, err := auth.GetUser(r.Context())
if err == nil {
pd.Authenticated = true
pd.Email = user.Email
pd.ID = strconv.FormatInt(user.ID, 10)
pd.DisplayName = user.Email
bookings, err := q.ListBookingsWithServiceByUser(r.Context(), user.ID)
if err != nil {
log.Printf("Error fetching bookings: %v", err)
} else {
for _, b := range bookings {
pd.Bookings = append(pd.Bookings, BookingWithService{
ID: b.ID,
ServiceName: b.ServiceName,
ServiceDescription: nullStringToString(b.ServiceDescription),
ServicePriceCents: b.ServicePriceCents,
EventDate: b.EventDate.Format("January 2, 2006"),
Address: nullStringToString(b.Address),
Notes: nullStringToString(b.Notes),
Status: b.Status,
CreatedAt: b.CreatedAt.Time.Format("Jan 2, 2006"),
})
}
}
oryAPIURL := os.Getenv("ORY_API_URL")
if oryAPIURL == "" {
tunnelPort := os.Getenv("TUNNEL_PORT")
if tunnelPort == "" {
tunnelPort = "4000"
}
oryAPIURL = "http://localhost:" + tunnelPort
}
pd.OrySettingsURL = oryAPIURL + "/ui/settings"
}
if err := tmpl.ExecuteTemplate(w, "profile.tmpl", pd); err != nil {
http.Error(w, "failed to render profile", http.StatusInternalServerError)
return
}
}
}