diff --git a/migrations/004_add_admin.js b/migrations/004_add_admin.js new file mode 100644 index 0000000..541e277 --- /dev/null +++ b/migrations/004_add_admin.js @@ -0,0 +1,13 @@ +/** @param {import('knex').Knex} knex */ +export function up(knex) { + return knex.schema.alterTable('users', (table) => { + table.boolean('admin').defaultTo(false).notNullable(); + }); +} + +/** @param {import('knex').Knex} knex */ +export function down(knex) { + return knex.schema.alterTable('users', (table) => { + table.dropColumn('admin'); + }); +} diff --git a/src/lib/server/airtable.ts b/src/lib/server/airtable.ts new file mode 100644 index 0000000..6301797 --- /dev/null +++ b/src/lib/server/airtable.ts @@ -0,0 +1,12 @@ +import { AIRTABLE_API_KEY, AIRTABLE_BASE_ID } from '$env/static/private'; + +export { AIRTABLE_API_KEY, AIRTABLE_BASE_ID }; + +export function escapeAirtableString(value: string): string { + return value.replace(/'/g, "''"); +} + +export function buildFilterFormula(field: string, value: string): string { + const safeValue = escapeAirtableString(value); + return encodeURIComponent(`{${field}} = '${safeValue}'`); +} diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 6c55332..cdb767e 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -24,8 +24,8 @@ export interface User { legal_first_name: string | null; legal_last_name: string | null; addresses: Address[]; - role: 'user' | 'admin'; snowflakes: number; + admin: boolean; created_at: Date; updated_at: Date; } diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts new file mode 100644 index 0000000..60922b7 --- /dev/null +++ b/src/routes/admin/+page.server.ts @@ -0,0 +1,262 @@ +import { redirect, fail } from '@sveltejs/kit'; +import { AIRTABLE_API_KEY, AIRTABLE_BASE_ID } from '$env/static/private'; +import type { PageServerLoad, Actions } from './$types'; + +interface ShopItemRecord { + id: string; + fields: { + 'Item Name': string; + }; +} + +interface ShopOrderRecord { + id: string; + fields: { + Name: string; + Item?: string[]; + Email: string; + 'Address Line 1': string; + 'Address Line 2'?: string; + City: string; + State: string; + Country: string; + 'Zip Code': string; + 'Phone Number': string; + Notes?: string; + Status?: string; + 'Shipping Information'?: string; + }; +} + +interface SnowflakeRecord { + id: string; + fields: { + Email?: string; + 'Snowflake Count'?: number; + }; +} + +const COMPLETED_STATUSES = ['Rejected With Refund', 'Rejected Without Refund', 'Shipped']; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) { + throw redirect(302, '/landing'); + } + + if (!locals.user.admin) { + throw redirect(302, '/'); + } + + const [ordersResponse, itemsResponse] = await Promise.all([ + fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop%20Orders`, { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + }), + fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop`, { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + }) + ]); + + if (!ordersResponse.ok) { + return { orders: [], error: 'Failed to fetch orders' }; + } + + const ordersData = await ordersResponse.json(); + const itemsData = itemsResponse.ok ? await itemsResponse.json() : { records: [] }; + + const itemsMap = new Map(); + for (const item of itemsData.records as ShopItemRecord[]) { + itemsMap.set(item.id, item.fields['Item Name']); + } + + const orders = (ordersData.records as ShopOrderRecord[]) + .filter((order) => !COMPLETED_STATUSES.includes(order.fields.Status ?? '')) + .map((order) => ({ + id: order.id, + name: order.fields.Name, + email: order.fields.Email, + itemId: order.fields.Item?.[0] ?? null, + itemName: order.fields.Item?.[0] ? (itemsMap.get(order.fields.Item[0]) ?? 'Unknown Item') : 'Unknown Item', + addressLine1: order.fields['Address Line 1'], + addressLine2: order.fields['Address Line 2'] ?? '', + city: order.fields.City, + state: order.fields.State, + country: order.fields.Country, + zipCode: order.fields['Zip Code'], + phoneNumber: order.fields['Phone Number'], + notes: order.fields.Notes ?? '', + status: order.fields.Status ?? 'Pending' + })); + + return { orders }; +}; + +export const actions: Actions = { + rejectWithRefund: async ({ request, locals }) => { + if (!locals.user?.admin) { + return fail(403, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const orderId = formData.get('orderId')?.toString(); + const userEmail = formData.get('userEmail')?.toString(); + const itemId = formData.get('itemId')?.toString(); + + if (!orderId || !userEmail || !itemId) { + return fail(400, { error: 'Missing required fields' }); + } + + const itemResponse = await fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop/${itemId}`, { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + }); + + let refundAmount = 0; + if (itemResponse.ok) { + const item = await itemResponse.json(); + const costMatch = item.fields?.Cost?.match(/^(\d+)\s+Snowflakes?$/i); + if (costMatch) { + refundAmount = parseInt(costMatch[1], 10); + } + } + + if (refundAmount > 0) { + const snowflakeResponse = await fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Snowflake%20Count`, + { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + } + ); + + if (snowflakeResponse.ok) { + const snowflakeData = await snowflakeResponse.json(); + const userRecord = snowflakeData.records?.find( + (r: SnowflakeRecord) => r.fields?.Email?.trim() === userEmail + ); + + if (userRecord) { + const currentBalance = userRecord.fields?.['Snowflake Count'] ?? 0; + await fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Snowflake%20Count/${userRecord.id}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fields: { + 'Snowflake Count': currentBalance + refundAmount + } + }) + } + ); + } + } + } + + const updateResponse = await fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop%20Orders/${orderId}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fields: { + Status: 'Rejected With Refund' + } + }) + } + ); + + if (!updateResponse.ok) { + return fail(500, { error: 'Failed to update order status' }); + } + + throw redirect(302, '/admin'); + }, + + rejectWithoutRefund: async ({ request, locals }) => { + if (!locals.user?.admin) { + return fail(403, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const orderId = formData.get('orderId')?.toString(); + + if (!orderId) { + return fail(400, { error: 'Missing order ID' }); + } + + const updateResponse = await fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop%20Orders/${orderId}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fields: { + Status: 'Rejected Without Refund' + } + }) + } + ); + + if (!updateResponse.ok) { + return fail(500, { error: 'Failed to update order status' }); + } + + throw redirect(302, '/admin'); + }, + + markShipped: async ({ request, locals }) => { + if (!locals.user?.admin) { + return fail(403, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const orderId = formData.get('orderId')?.toString(); + const trackingLabel = formData.get('trackingLabel')?.toString(); + + if (!orderId || !trackingLabel) { + return fail(400, { error: 'Missing required fields' }); + } + + const updateResponse = await fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop%20Orders/${orderId}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fields: { + Status: 'Shipped', + 'Shipping Information': trackingLabel + } + }) + } + ); + + if (!updateResponse.ok) { + return fail(500, { error: 'Failed to update order status' }); + } + + throw redirect(302, '/admin'); + } +}; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100644 index 0000000..f36a7f3 --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,400 @@ + + +
+

Order Admin

+ + {#if form?.error} +

{form.error}

+ {/if} + + {#if form?.success} +

Order updated successfully!

+ {/if} + + {#if selectedOrder} +
+ + +
+

Order Details

+ +
+ Item: {selectedOrder.itemName} +
+
+ Shipping Name: {selectedOrder.name} +
+
+ Email: {selectedOrder.email} +
+
+ Phone: {selectedOrder.phoneNumber} +
+
+ Address:
+ {selectedOrder.addressLine1}
+ {#if selectedOrder.addressLine2} + {selectedOrder.addressLine2}
+ {/if} + {selectedOrder.city}, {selectedOrder.state} {selectedOrder.zipCode}
+ {selectedOrder.country} +
+ {#if selectedOrder.notes} +
+ Notes: {selectedOrder.notes} +
+ {/if} +
+ Status: {selectedOrder.status} +
+ +
+
+ + + + +
+ +
+ + +
+ + +
+
+
+ + {#if showTrackingModal} + + {/if} + {:else} +
+ {#if data.orders.length === 0} +

No pending orders to review.

+ {:else} + {#each data.orders as order} + + {/each} + {/if} +
+ {/if} + + +
+ + diff --git a/src/routes/auth/callback/+server.ts b/src/routes/auth/callback/+server.ts index be82b9a..ec4ed7f 100644 --- a/src/routes/auth/callback/+server.ts +++ b/src/routes/auth/callback/+server.ts @@ -47,8 +47,8 @@ export const GET: RequestHandler = async ({ url, cookies }) => { legal_first_name: identity.legal_first_name, legal_last_name: identity.legal_last_name, addresses: JSON.stringify(identity.addresses || []) as unknown as undefined, - role: 'user' - }) + admin: false + }) .returning('*'); user = insertedUser; } else { diff --git a/src/routes/profile/+page.server.ts b/src/routes/profile/+page.server.ts index 112c6b0..19422bb 100644 --- a/src/routes/profile/+page.server.ts +++ b/src/routes/profile/+page.server.ts @@ -1,5 +1,5 @@ import { redirect } from '@sveltejs/kit'; -import { AIRTABLE_API_KEY, AIRTABLE_BASE_ID } from '$env/static/private'; +import { AIRTABLE_API_KEY, AIRTABLE_BASE_ID, buildFilterFormula } from '$lib/server/airtable'; import type { PageServerLoad } from './$types'; interface AirtableAttachment { @@ -41,32 +41,54 @@ export const load: PageServerLoad = async ({ locals }) => { throw redirect(302, '/landing'); } - const filterFormula = encodeURIComponent(`{Email} = '${locals.user.email}'`); + const filterFormula = buildFilterFormula('Email', locals.user.email); - const response = await fetch( - `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/YSWS%20Project%20Submission?filterByFormula=${filterFormula}`, - { - headers: { - Authorization: `Bearer ${AIRTABLE_API_KEY}`, - 'Content-Type': 'application/json' + const [projectsResponse, snowflakeResponse] = await Promise.all([ + fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/YSWS%20Project%20Submission?filterByFormula=${filterFormula}`, + { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } } - } - ); + ), + fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Snowflake%20Count`, + { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + } + ) + ]); - if (!response.ok) { - console.error('Airtable fetch failed:', response.statusText); - return { projects: [] }; - } + let projects: Project[] = []; + let snowflakeCount = 0; - const data: AirtableResponse = await response.json(); + if (projectsResponse.ok) { + const data: AirtableResponse = await projectsResponse.json(); + projects = data.records.map((record) => ({ + id: record.id, + codeUrl: record.fields['Code URL'] ?? null, + playableUrl: record.fields['Playable URL'] ?? null, + screenshot: record.fields['Screenshot']?.[0]?.url ?? null, + status: record.fields['Automation - Status'] === '2–Submitted' ? 'Approved' : 'Pending Review' + })); + } else { + console.error('Airtable projects fetch failed:', projectsResponse.statusText); + } - const projects: Project[] = data.records.map((record) => ({ - id: record.id, - codeUrl: record.fields['Code URL'] ?? null, - playableUrl: record.fields['Playable URL'] ?? null, - screenshot: record.fields['Screenshot']?.[0]?.url ?? null, - status: record.fields['Automation - Status'] === '2–Submitted' ? 'Approved' : 'Pending Review' - })); + if (snowflakeResponse.ok) { + const snowflakeData = await snowflakeResponse.json(); + const userRecord = snowflakeData.records?.find( + (r: { fields?: { Email?: string } }) => r.fields?.Email?.trim() === locals.user.email + ); + snowflakeCount = userRecord?.fields?.['Snowflake Count'] ?? 0; + } else { + console.error('Airtable snowflake count fetch failed:', snowflakeResponse.statusText); + } - return { projects }; + return { projects, snowflakeCount }; }; diff --git a/src/routes/profile/+page.svelte b/src/routes/profile/+page.svelte index e8f7035..8288f36 100644 --- a/src/routes/profile/+page.svelte +++ b/src/routes/profile/+page.svelte @@ -13,7 +13,7 @@

Snowflakes

-
{data.user?.snowflakes ?? 0}
+
{data.snowflakeCount ?? 0}
diff --git a/src/routes/shop/+page.server.ts b/src/routes/shop/+page.server.ts index 796f418..0e36b72 100644 --- a/src/routes/shop/+page.server.ts +++ b/src/routes/shop/+page.server.ts @@ -40,22 +40,27 @@ export const load: PageServerLoad = async ({ locals }) => { throw redirect(302, '/landing'); } - const response = await fetch( - `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop`, - { + const [shopResponse, snowflakeResponse] = await Promise.all([ + fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop`, { headers: { Authorization: `Bearer ${AIRTABLE_API_KEY}`, 'Content-Type': 'application/json' } - } - ); + }), + fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Snowflake%20Count`, { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + }) + ]); - if (!response.ok) { - console.error('Airtable fetch failed:', response.statusText); - return { items: [] }; + if (!shopResponse.ok) { + console.error('Airtable shop fetch failed:', shopResponse.statusText); + return { items: [], snowflakeCount: 0 }; } - const data: AirtableResponse = await response.json(); + const data: AirtableResponse = await shopResponse.json(); const items: ShopItem[] = data.records .map((record) => ({ @@ -67,5 +72,15 @@ export const load: PageServerLoad = async ({ locals }) => { })) .sort((a, b) => a.order - b.order); - return { items }; + let snowflakeCount = 0; + if (snowflakeResponse.ok) { + const snowflakeData = await snowflakeResponse.json(); + const userRecord = snowflakeData.records?.find( + (r: { fields?: { Email?: string } }) => + r.fields?.Email?.trim() === locals.user.email + ); + snowflakeCount = userRecord?.fields?.['Snowflake Count'] ?? 0; + } + + return { items, snowflakeCount }; }; diff --git a/src/routes/shop/+page.svelte b/src/routes/shop/+page.svelte index b575ed6..1712974 100644 --- a/src/routes/shop/+page.svelte +++ b/src/routes/shop/+page.svelte @@ -6,14 +6,30 @@ function goBack() { window.location.href = '/'; } + + function parseSnowflakeCost(cost: string): number | null { + const match = cost.match(/^(\d+)\s+Snowflakes?$/i); + return match ? parseInt(match[1], 10) : null; + } + + function canAfford(cost: string): boolean { + const snowflakeCost = parseSnowflakeCost(cost); + if (snowflakeCost === null) return false; + return data.snowflakeCount >= snowflakeCost; + } + + function isSnowflakeCost(cost: string): boolean { + return parseSnowflakeCost(cost) !== null; + }

Shop

+

snowflake {data.snowflakeCount} Snowflakes

{#each data.items as item} -
+
{#if item.image} {item.name} {:else} @@ -22,6 +38,13 @@

{item.name}

{item.cost}

+ {#if isSnowflakeCost(item.cost)} + {#if canAfford(item.cost)} + Buy + {:else} +

Not enough snowflakes

+ {/if} + {/if}
{/each} @@ -54,11 +77,28 @@ h1 { color: #fff; font-size: 3rem; - margin-bottom: 2.5rem; + margin-bottom: 0.5rem; text-align: center; padding: 0 1rem; } + .snowflake-balance { + color: #f1c40f; + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 2rem; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + } + + .snowflake-icon { + width: 1.5rem; + height: 1.5rem; + } + .items-grid { display: grid; grid-template-columns: repeat(3, 1fr); @@ -137,6 +177,43 @@ font-weight: bold; } + .item-card.disabled { + opacity: 0.5; + pointer-events: none; + } + + .item-card.disabled:hover { + transform: none; + background: rgba(255, 255, 255, 0.1); + } + + .buy-btn { + display: inline-block; + margin-top: 0.75rem; + background: #33d6a6; + border: none; + border-radius: 5px; + color: #fff; + padding: 0.5rem 1.5rem; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + font-family: inherit; + transition: background 0.2s; + text-decoration: none; + } + + .buy-btn:hover { + background: #2ab890; + } + + .insufficient { + margin: 0.5rem 0 0; + color: rgba(255, 255, 255, 0.6); + font-size: 0.9rem; + font-style: italic; + } + .empty-message { color: rgba(255, 255, 255, 0.8); font-size: 1.2rem; diff --git a/src/routes/shop/order/[itemId]/+page.server.ts b/src/routes/shop/order/[itemId]/+page.server.ts new file mode 100644 index 0000000..cc227c0 --- /dev/null +++ b/src/routes/shop/order/[itemId]/+page.server.ts @@ -0,0 +1,230 @@ +import { redirect, fail } from '@sveltejs/kit'; +import { AIRTABLE_API_KEY, AIRTABLE_BASE_ID } from '$env/static/private'; +import type { PageServerLoad, Actions } from './$types'; + +interface AirtableAttachment { + id: string; + url: string; + filename: string; + type: string; +} + +interface ShopItemRecord { + id: string; + fields: { + 'Item Name': string; + Cost: string; + Image?: AirtableAttachment[]; + }; +} + +interface SnowflakeRecord { + id: string; + fields: { + Email?: string; + 'Snowflake Count'?: number; + }; +} + +export const load: PageServerLoad = async ({ locals, params }) => { + if (!locals.user) { + throw redirect(302, '/landing'); + } + + const [itemResponse, snowflakeResponse] = await Promise.all([ + fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop/${params.itemId}`, { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + }), + fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Snowflake%20Count`, { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + }) + ]); + + if (!itemResponse.ok) { + throw redirect(302, '/shop'); + } + + const item: ShopItemRecord = await itemResponse.json(); + + const costMatch = item.fields.Cost.match(/^(\d+)\s+Snowflakes?$/i); + if (!costMatch) { + throw redirect(302, '/shop'); + } + + const cost = parseInt(costMatch[1], 10); + + let snowflakeCount = 0; + let snowflakeRecordId: string | null = null; + + if (snowflakeResponse.ok) { + const snowflakeData = await snowflakeResponse.json(); + const userRecord = snowflakeData.records?.find( + (r: SnowflakeRecord) => r.fields?.Email?.trim() === locals.user.email + ); + if (userRecord) { + snowflakeCount = userRecord.fields?.['Snowflake Count'] ?? 0; + snowflakeRecordId = userRecord.id; + } + } + + if (snowflakeCount < cost) { + throw redirect(302, '/shop'); + } + + return { + item: { + id: item.id, + name: item.fields['Item Name'], + cost: item.fields.Cost, + costValue: cost, + image: item.fields.Image?.[0]?.url ?? null + }, + email: locals.user.email, + snowflakeCount, + snowflakeRecordId + }; +}; + +export const actions: Actions = { + default: async ({ request, locals, params }) => { + if (!locals.user) { + throw redirect(302, '/landing'); + } + + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim(); + const addressLine1 = formData.get('addressLine1')?.toString().trim(); + const addressLine2 = formData.get('addressLine2')?.toString().trim() || ''; + const city = formData.get('city')?.toString().trim(); + const state = formData.get('state')?.toString().trim(); + const country = formData.get('country')?.toString().trim(); + const zipCode = formData.get('zipCode')?.toString().trim(); + const phoneNumber = formData.get('phoneNumber')?.toString().trim(); + const notes = formData.get('notes')?.toString().trim() || ''; + + if (!name || !addressLine1 || !city || !state || !country || !zipCode || !phoneNumber) { + return fail(400, { error: 'Please fill in all required fields' }); + } + + const [itemResponse, snowflakeResponse] = await Promise.all([ + fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop/${params.itemId}`, { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + }), + fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Snowflake%20Count`, { + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + } + }) + ]); + + if (!itemResponse.ok) { + return fail(400, { error: 'Item not found' }); + } + + const item: ShopItemRecord = await itemResponse.json(); + + const costMatch = item.fields.Cost.match(/^(\d+)\s+Snowflakes?$/i); + if (!costMatch) { + return fail(400, { error: 'This item cannot be purchased with snowflakes' }); + } + + const cost = parseInt(costMatch[1], 10); + + if (!snowflakeResponse.ok) { + return fail(500, { error: 'Failed to verify snowflake balance' }); + } + + const snowflakeData = await snowflakeResponse.json(); + const userRecord = snowflakeData.records?.find( + (r: SnowflakeRecord) => r.fields?.Email?.trim() === locals.user.email + ); + + if (!userRecord) { + return fail(400, { error: 'Snowflake balance not found' }); + } + + const currentBalance = userRecord.fields?.['Snowflake Count'] ?? 0; + + if (currentBalance < cost) { + return fail(400, { error: 'Not enough snowflakes' }); + } + + const newBalance = currentBalance - cost; + const updateResponse = await fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Snowflake%20Count/${userRecord.id}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fields: { + 'Snowflake Count': newBalance + } + }) + } + ); + + if (!updateResponse.ok) { + return fail(500, { error: 'Failed to update snowflake balance' }); + } + + const orderResponse = await fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Shop%20Orders`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fields: { + Name: name, + Item: [params.itemId], + Email: locals.user.email, + 'Address Line 1': addressLine1, + 'Address Line 2': addressLine2, + City: city, + State: state, + Country: country, + 'Zip Code': zipCode, + 'Phone Number': phoneNumber, + Notes: notes + } + }) + } + ); + + if (!orderResponse.ok) { + await fetch( + `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/Snowflake%20Count/${userRecord.id}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${AIRTABLE_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fields: { + 'Snowflake Count': currentBalance + } + }) + } + ); + return fail(500, { error: 'Failed to create order' }); + } + + throw redirect(302, '/shop?success=true'); + } +}; diff --git a/src/routes/shop/order/[itemId]/+page.svelte b/src/routes/shop/order/[itemId]/+page.svelte new file mode 100644 index 0000000..5e20e2f --- /dev/null +++ b/src/routes/shop/order/[itemId]/+page.svelte @@ -0,0 +1,305 @@ + + +
+

Order: {data.item.name}

+ +
+
+ {#if data.item.image} + {data.item.name} + {:else} +
Image Coming Soon
+ {/if} +

{data.item.cost}

+

Your balance: {data.snowflakeCount} Snowflakes

+

After purchase: {data.snowflakeCount - data.item.costValue} Snowflakes

+
+ +
{ + submitting = true; + return async ({ update }) => { + submitting = false; + await update(); + }; + }} + > + {#if form?.error} +

{form.error}

+ {/if} + + + + + + + + + + + +
+ + + +
+ +
+ + + +
+ + + + +
+
+ + ← Back to Shop +
+ +