detail page port

This commit is contained in:
nicwands
2026-05-29 11:22:56 -04:00
parent b85d28c142
commit e22d75c50a
65 changed files with 7006 additions and 4044 deletions

44
composables/prismic.js Normal file
View File

@@ -0,0 +1,44 @@
import * as prismic from '@prismicio/client'
// === Cached API of any pris method === //
const cache = {}
export const cacheResult = (method, ...args) => {
const key = method + JSON.stringify(args || [])
// If not in cache,
// fetch and store
if (!cache[key]) {
cache[key] = () => {
const client = prismic.createClient('swang')
return client[method](...args).catch((err) => {
console.log(`Error getting Prismic result ${key}:`, err)
throw err
})
}
}
return { key, execute: cache[key] }
}
// Get single doc
export const usePrisSingle = (type, options = {}) => {
const { key, execute } = cacheResult('getSingle', type, options)
return useAsyncData(key, execute, { deep: false })
}
// Get settings (assumed single doc)
export const usePrisSettings = () => {
return usePrisSingle('settings')
}
// Get all docs of a certain type
export const usePrisType = (type, options = {}) => {
const { key, execute } = cacheResult('getAllByType', type, options)
return useAsyncData(key, execute, { deep: false })
}
// Get doc by UID (and type)
export const usePrisUid = (uid, type = 'page', options = {}) => {
const { key, execute } = cacheResult('getByUID', type, uid, options)
return useAsyncData(key, execute, { deep: false })
}

View File

@@ -1,42 +0,0 @@
import { ref } from 'vue'
export default () => {
const os = ref('Unknown')
const isMobile = ref(false)
const isAndroid = ref(false)
const isIOS = ref(false)
const isMacOS = ref(false)
const isWindows = ref(false)
const isLinux = ref(false)
if (typeof navigator !== 'undefined') {
const userAgent = navigator.userAgent.toLowerCase()
const platform = navigator.platform.toLowerCase()
const detectedAndroid = /android/.test(userAgent)
const detectedIOS = /iphone|ipad|ipod/.test(userAgent)
const detectedMacOS = /mac/.test(platform) && !detectedIOS
const detectedWindows = /win/.test(platform)
const detectedLinux = /linux/.test(platform) && !detectedAndroid
os.value = detectedAndroid
? 'Android'
: detectedIOS
? 'iOS'
: detectedMacOS
? 'macOS'
: detectedWindows
? 'Windows'
: detectedLinux
? 'Linux'
: 'Unknown'
isMobile.value = detectedAndroid || detectedIOS
isAndroid.value = detectedAndroid
isIOS.value = detectedIOS
isMacOS.value = detectedMacOS
isWindows.value = detectedWindows
isLinux.value = detectedLinux
}
return { os, isMobile, isAndroid, isIOS, isMacOS, isWindows, isLinux }
}

107
composables/useKlaviyo.js Normal file
View File

@@ -0,0 +1,107 @@
import { ref } from 'vue'
// Wraps Klaviyo's legacy client-side list-subscribe endpoint.
// Klaviyo requires any non-standard property names to be declared in `$fields`
// for them to be persisted on the profile — this composable handles that for you.
const ENDPOINT = 'https://manage.kmail-lists.com/ajax/subscriptions/subscribe'
export default () => {
const loading = ref(false)
const success = ref(false)
const error = ref('')
const reset = () => {
loading.value = false
success.value = false
error.value = ''
}
const subscribe = async ({
listId,
email,
firstName,
lastName,
name,
source,
properties = {},
} = {}) => {
if (!listId) {
error.value = 'Missing listId'
return
}
if (!email) {
error.value = 'Missing email'
return
}
loading.value = true
error.value = ''
// `name` is a convenience: first token → first_name, rest → last_name
let fName = firstName
let lName = lastName
if (name && !fName && !lName) {
const parts = name.trim().split(/\s+/)
fName = parts.shift()
lName = parts.join(' ') || undefined
}
const body = { g: listId, email }
if (fName) body.first_name = fName
if (lName) body.last_name = lName
const extraFields = []
if (source) {
body.$source = source
extraFields.push('$source')
}
for (const [key, value] of Object.entries(properties)) {
if (value === undefined || value === null || value === '') continue
body[key] = value
extraFields.push(key)
}
if (extraFields.length) body.$fields = extraFields.join(',')
try {
await $fetch(ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cache-Control': 'no-cache',
},
body: new URLSearchParams(body),
})
success.value = true
} catch (err) {
console.error('useKlaviyo subscribe error', err)
error.value =
err?.toString().split('Error: ')[1] || 'Subscription failed'
}
loading.value = false
}
// Fires a Klaviyo event (metric) — append-only history, good for things
// segments will slice on like "Submitted RSVP" with per-event properties.
// Throws on failure; doesn't touch the shared loading/success/error refs,
// so callers can pair it with subscribe() without state collisions.
const track = ({ email, metric, properties = {} }) => {
if (!email || !metric) {
throw new Error('useKlaviyo.track: email and metric are required')
}
return $fetch('/api/klaviyo-track', {
method: 'POST',
body: { email, metric, properties },
})
}
return {
loading,
success,
error,
subscribe,
track,
reset,
}
}