feat(convex): add user text preferences API and storage
- commit
- 39c9c538278895a10f8e156b5e6ae66a1e6d27e0
- author
- bnema · · 2026-01-18 18:56 UTC
- parent
- 1e38826
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +308 -19
M src/convex/_generated/api.d.ts +2 -0
| @@ -15,6 +15,7 @@ import type * as messagesInternal from "../messagesInternal.js"; | |||
| 15 | 15 | import type * as mutations from "../mutations.js"; | |
| 16 | 16 | import type * as queries from "../queries.js"; | |
| 17 | 17 | import type * as sessionsInternal from "../sessionsInternal.js"; | |
| 18 | + | import type * as textPreferencesInternal from "../textPreferencesInternal.js"; | |
| 18 | 19 | import type * as usersInternal from "../usersInternal.js"; | |
| 19 | 20 | ||
| 20 | 21 | import type { | |
| @@ -31,6 +32,7 @@ declare const fullApi: ApiFromModules<{ | |||
| 31 | 32 | mutations: typeof mutations; | |
| 32 | 33 | queries: typeof queries; | |
| 33 | 34 | sessionsInternal: typeof sessionsInternal; | |
| 35 | + | textPreferencesInternal: typeof textPreferencesInternal; | |
| 34 | 36 | usersInternal: typeof usersInternal; | |
| 35 | 37 | }>; | |
| 36 | 38 | ||
| @@ -15,6 +15,7 @@ import type * as messagesInternal from "../messagesInternal.js"; | |||||
| 15 | import type * as mutations from "../mutations.js"; | 15 | import type * as mutations from "../mutations.js"; | ||
| 16 | import type * as queries from "../queries.js"; | 16 | import type * as queries from "../queries.js"; | ||
| 17 | import type * as sessionsInternal from "../sessionsInternal.js"; | 17 | import type * as sessionsInternal from "../sessionsInternal.js"; | ||
| 18 | + | import type * as textPreferencesInternal from "../textPreferencesInternal.js"; | |||
| 18 | import type * as usersInternal from "../usersInternal.js"; | 19 | import type * as usersInternal from "../usersInternal.js"; | ||
| 19 | 20 | ||||
| 20 | import type { | 21 | import type { | ||
| @@ -31,6 +32,7 @@ declare const fullApi: ApiFromModules<{ | |||||
| 31 | mutations: typeof mutations; | 32 | mutations: typeof mutations; | ||
| 32 | queries: typeof queries; | 33 | queries: typeof queries; | ||
| 33 | sessionsInternal: typeof sessionsInternal; | 34 | sessionsInternal: typeof sessionsInternal; | ||
| 35 | + | textPreferencesInternal: typeof textPreferencesInternal; | |||
| 34 | usersInternal: typeof usersInternal; | 36 | usersInternal: typeof usersInternal; | ||
| 35 | }>; | 37 | }>; | ||
| 36 | 38 | ||||
M src/convex/http.ts +44 -0
| @@ -275,4 +275,48 @@ http.route({ | |||
| 275 | 275 | }) | |
| 276 | 276 | }); | |
| 277 | 277 | ||
| 278 | + | // ============ TEXT PREFERENCES (server-side only) ============ | |
| 279 | + | ||
| 280 | + | // Get user text preferences | |
| 281 | + | http.route({ | |
| 282 | + | path: '/api/textPreferences/get', | |
| 283 | + | method: 'POST', | |
| 284 | + | handler: httpAction(async (ctx, request) => { | |
| 285 | + | if (!validateSecret(request)) return unauthorizedResponse(); | |
| 286 | + | ||
| 287 | + | const { userId } = await request.json(); | |
| 288 | + | const preferences = await ctx.runQuery(internal.textPreferencesInternal.getByUserId, { | |
| 289 | + | userId | |
| 290 | + | }); | |
| 291 | + | ||
| 292 | + | return new Response(JSON.stringify(preferences), { | |
| 293 | + | headers: { 'Content-Type': 'application/json' } | |
| 294 | + | }); | |
| 295 | + | }) | |
| 296 | + | }); | |
| 297 | + | ||
| 298 | + | // Save user text preferences | |
| 299 | + | http.route({ | |
| 300 | + | path: '/api/textPreferences/save', | |
| 301 | + | method: 'POST', | |
| 302 | + | handler: httpAction(async (ctx, request) => { | |
| 303 | + | if (!validateSecret(request)) return unauthorizedResponse(); | |
| 304 | + | ||
| 305 | + | const { userId, defaultStyle, allowFormatting, maxMessageLength } = await request.json(); | |
| 306 | + | try { | |
| 307 | + | const result = await ctx.runMutation(internal.textPreferencesInternal.upsert, { | |
| 308 | + | userId, | |
| 309 | + | defaultStyle, | |
| 310 | + | allowFormatting, | |
| 311 | + | maxMessageLength | |
| 312 | + | }); | |
| 313 | + | return new Response(JSON.stringify({ success: true, id: result }), { | |
| 314 | + | headers: { 'Content-Type': 'application/json' } | |
| 315 | + | }); | |
| 316 | + | } catch (e) { | |
| 317 | + | return errorResponse(e instanceof Error ? e.message : 'Failed to save preferences'); | |
| 318 | + | } | |
| 319 | + | }) | |
| 320 | + | }); | |
| 321 | + | ||
| 278 | 322 | export default http; | |
| @@ -275,4 +275,48 @@ http.route({ | |||||
| 275 | }) | 275 | }) | ||
| 276 | }); | 276 | }); | ||
| 277 | 277 | ||||
| 278 | + | // ============ TEXT PREFERENCES (server-side only) ============ | |||
| 279 | + | ||||
| 280 | + | // Get user text preferences | |||
| 281 | + | http.route({ | |||
| 282 | + | path: '/api/textPreferences/get', | |||
| 283 | + | method: 'POST', | |||
| 284 | + | handler: httpAction(async (ctx, request) => { | |||
| 285 | + | if (!validateSecret(request)) return unauthorizedResponse(); | |||
| 286 | + | ||||
| 287 | + | const { userId } = await request.json(); | |||
| 288 | + | const preferences = await ctx.runQuery(internal.textPreferencesInternal.getByUserId, { | |||
| 289 | + | userId | |||
| 290 | + | }); | |||
| 291 | + | ||||
| 292 | + | return new Response(JSON.stringify(preferences), { | |||
| 293 | + | headers: { 'Content-Type': 'application/json' } | |||
| 294 | + | }); | |||
| 295 | + | }) | |||
| 296 | + | }); | |||
| 297 | + | ||||
| 298 | + | // Save user text preferences | |||
| 299 | + | http.route({ | |||
| 300 | + | path: '/api/textPreferences/save', | |||
| 301 | + | method: 'POST', | |||
| 302 | + | handler: httpAction(async (ctx, request) => { | |||
| 303 | + | if (!validateSecret(request)) return unauthorizedResponse(); | |||
| 304 | + | ||||
| 305 | + | const { userId, defaultStyle, allowFormatting, maxMessageLength } = await request.json(); | |||
| 306 | + | try { | |||
| 307 | + | const result = await ctx.runMutation(internal.textPreferencesInternal.upsert, { | |||
| 308 | + | userId, | |||
| 309 | + | defaultStyle, | |||
| 310 | + | allowFormatting, | |||
| 311 | + | maxMessageLength | |||
| 312 | + | }); | |||
| 313 | + | return new Response(JSON.stringify({ success: true, id: result }), { | |||
| 314 | + | headers: { 'Content-Type': 'application/json' } | |||
| 315 | + | }); | |||
| 316 | + | } catch (e) { | |||
| 317 | + | return errorResponse(e instanceof Error ? e.message : 'Failed to save preferences'); | |||
| 318 | + | } | |||
| 319 | + | }) | |||
| 320 | + | }); | |||
| 321 | + | ||||
| 278 | export default http; | 322 | export default http; | ||
M src/convex/schema.ts +12 -6
| @@ -40,12 +40,18 @@ export default defineSchema({ | |||
| 40 | 40 | ||
| 41 | 41 | userTextPreferences: defineTable({ | |
| 42 | 42 | userId: v.id('users'), | |
| 43 | - | defaultFontFamily: v.optional(v.string()), | |
| 44 | - | defaultFontSize: v.optional(v.number()), | |
| 45 | - | defaultColor: v.optional(v.string()), | |
| 46 | - | allowFormatting: v.optional(v.boolean()), | |
| 47 | - | maxMessageLength: v.optional(v.number()), | |
| 48 | - | stylePresets: v.optional(v.string()), | |
| 43 | + | defaultStyle: v.object({ | |
| 44 | + | fontFamily: v.string(), | |
| 45 | + | fontSize: v.number(), | |
| 46 | + | color: v.optional(v.string()), | |
| 47 | + | gradient: v.optional(v.array(v.string())), | |
| 48 | + | bold: v.boolean(), | |
| 49 | + | italic: v.boolean(), | |
| 50 | + | underline: v.boolean(), | |
| 51 | + | strikethrough: v.boolean() | |
| 52 | + | }), | |
| 53 | + | allowFormatting: v.boolean(), | |
| 54 | + | maxMessageLength: v.number(), | |
| 49 | 55 | createdAt: v.number(), | |
| 50 | 56 | updatedAt: v.number() | |
| 51 | 57 | }).index('by_userId', ['userId']) | |
| @@ -40,12 +40,18 @@ export default defineSchema({ | |||||
| 40 | 40 | ||||
| 41 | userTextPreferences: defineTable({ | 41 | userTextPreferences: defineTable({ | ||
| 42 | userId: v.id('users'), | 42 | userId: v.id('users'), | ||
| 43 | - | defaultFontFamily: v.optional(v.string()), | 43 | + | defaultStyle: v.object({ |
| 44 | - | defaultFontSize: v.optional(v.number()), | 44 | + | fontFamily: v.string(), |
| 45 | - | defaultColor: v.optional(v.string()), | 45 | + | fontSize: v.number(), |
| 46 | - | allowFormatting: v.optional(v.boolean()), | 46 | + | color: v.optional(v.string()), |
| 47 | - | maxMessageLength: v.optional(v.number()), | 47 | + | gradient: v.optional(v.array(v.string())), |
| 48 | - | stylePresets: v.optional(v.string()), | 48 | + | bold: v.boolean(), |
| 49 | + | italic: v.boolean(), | |||
| 50 | + | underline: v.boolean(), | |||
| 51 | + | strikethrough: v.boolean() | |||
| 52 | + | }), | |||
| 53 | + | allowFormatting: v.boolean(), | |||
| 54 | + | maxMessageLength: v.number(), | |||
| 49 | createdAt: v.number(), | 55 | createdAt: v.number(), | ||
| 50 | updatedAt: v.number() | 56 | updatedAt: v.number() | ||
| 51 | }).index('by_userId', ['userId']) | 57 | }).index('by_userId', ['userId']) | ||
A src/convex/textPreferencesInternal.ts +63 -0
| @@ -0,0 +1,63 @@ | |||
| 1 | + | import { internalQuery, internalMutation } from './_generated/server'; | |
| 2 | + | import { v } from 'convex/values'; | |
| 3 | + | ||
| 4 | + | // Get user text preferences by userId | |
| 5 | + | export const getByUserId = internalQuery({ | |
| 6 | + | args: { userId: v.id('users') }, | |
| 7 | + | handler: async (ctx, { userId }) => { | |
| 8 | + | const preferences = await ctx.db | |
| 9 | + | .query('userTextPreferences') | |
| 10 | + | .withIndex('by_userId', (q) => q.eq('userId', userId)) | |
| 11 | + | .first(); | |
| 12 | + | ||
| 13 | + | return preferences; | |
| 14 | + | } | |
| 15 | + | }); | |
| 16 | + | ||
| 17 | + | // Upsert user text preferences (create or update) | |
| 18 | + | export const upsert = internalMutation({ | |
| 19 | + | args: { | |
| 20 | + | userId: v.id('users'), | |
| 21 | + | defaultStyle: v.object({ | |
| 22 | + | fontFamily: v.string(), | |
| 23 | + | fontSize: v.number(), | |
| 24 | + | color: v.optional(v.string()), | |
| 25 | + | gradient: v.optional(v.array(v.string())), | |
| 26 | + | bold: v.boolean(), | |
| 27 | + | italic: v.boolean(), | |
| 28 | + | underline: v.boolean(), | |
| 29 | + | strikethrough: v.boolean() | |
| 30 | + | }), | |
| 31 | + | allowFormatting: v.boolean(), | |
| 32 | + | maxMessageLength: v.number() | |
| 33 | + | }, | |
| 34 | + | handler: async (ctx, args) => { | |
| 35 | + | const now = Date.now(); | |
| 36 | + | ||
| 37 | + | // Check if preferences already exist | |
| 38 | + | const existing = await ctx.db | |
| 39 | + | .query('userTextPreferences') | |
| 40 | + | .withIndex('by_userId', (q) => q.eq('userId', args.userId)) | |
| 41 | + | .first(); | |
| 42 | + | ||
| 43 | + | if (existing) { | |
| 44 | + | // Update existing preferences | |
| 45 | + | return await ctx.db.patch(existing._id, { | |
| 46 | + | defaultStyle: args.defaultStyle, | |
| 47 | + | allowFormatting: args.allowFormatting, | |
| 48 | + | maxMessageLength: args.maxMessageLength, | |
| 49 | + | updatedAt: now | |
| 50 | + | }); | |
| 51 | + | } else { | |
| 52 | + | // Create new preferences | |
| 53 | + | return await ctx.db.insert('userTextPreferences', { | |
| 54 | + | userId: args.userId, | |
| 55 | + | defaultStyle: args.defaultStyle, | |
| 56 | + | allowFormatting: args.allowFormatting, | |
| 57 | + | maxMessageLength: args.maxMessageLength, | |
| 58 | + | createdAt: now, | |
| 59 | + | updatedAt: now | |
| 60 | + | }); | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | }); | |
| @@ -0,0 +1,63 @@ | |||||
| 1 | + | import { internalQuery, internalMutation } from './_generated/server'; | |||
| 2 | + | import { v } from 'convex/values'; | |||
| 3 | + | ||||
| 4 | + | // Get user text preferences by userId | |||
| 5 | + | export const getByUserId = internalQuery({ | |||
| 6 | + | args: { userId: v.id('users') }, | |||
| 7 | + | handler: async (ctx, { userId }) => { | |||
| 8 | + | const preferences = await ctx.db | |||
| 9 | + | .query('userTextPreferences') | |||
| 10 | + | .withIndex('by_userId', (q) => q.eq('userId', userId)) | |||
| 11 | + | .first(); | |||
| 12 | + | ||||
| 13 | + | return preferences; | |||
| 14 | + | } | |||
| 15 | + | }); | |||
| 16 | + | ||||
| 17 | + | // Upsert user text preferences (create or update) | |||
| 18 | + | export const upsert = internalMutation({ | |||
| 19 | + | args: { | |||
| 20 | + | userId: v.id('users'), | |||
| 21 | + | defaultStyle: v.object({ | |||
| 22 | + | fontFamily: v.string(), | |||
| 23 | + | fontSize: v.number(), | |||
| 24 | + | color: v.optional(v.string()), | |||
| 25 | + | gradient: v.optional(v.array(v.string())), | |||
| 26 | + | bold: v.boolean(), | |||
| 27 | + | italic: v.boolean(), | |||
| 28 | + | underline: v.boolean(), | |||
| 29 | + | strikethrough: v.boolean() | |||
| 30 | + | }), | |||
| 31 | + | allowFormatting: v.boolean(), | |||
| 32 | + | maxMessageLength: v.number() | |||
| 33 | + | }, | |||
| 34 | + | handler: async (ctx, args) => { | |||
| 35 | + | const now = Date.now(); | |||
| 36 | + | ||||
| 37 | + | // Check if preferences already exist | |||
| 38 | + | const existing = await ctx.db | |||
| 39 | + | .query('userTextPreferences') | |||
| 40 | + | .withIndex('by_userId', (q) => q.eq('userId', args.userId)) | |||
| 41 | + | .first(); | |||
| 42 | + | ||||
| 43 | + | if (existing) { | |||
| 44 | + | // Update existing preferences | |||
| 45 | + | return await ctx.db.patch(existing._id, { | |||
| 46 | + | defaultStyle: args.defaultStyle, | |||
| 47 | + | allowFormatting: args.allowFormatting, | |||
| 48 | + | maxMessageLength: args.maxMessageLength, | |||
| 49 | + | updatedAt: now | |||
| 50 | + | }); | |||
| 51 | + | } else { | |||
| 52 | + | // Create new preferences | |||
| 53 | + | return await ctx.db.insert('userTextPreferences', { | |||
| 54 | + | userId: args.userId, | |||
| 55 | + | defaultStyle: args.defaultStyle, | |||
| 56 | + | allowFormatting: args.allowFormatting, | |||
| 57 | + | maxMessageLength: args.maxMessageLength, | |||
| 58 | + | createdAt: now, | |||
| 59 | + | updatedAt: now | |||
| 60 | + | }); | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | }); | |||
M src/lib/components/user-preferences-modal.svelte +11 -3
| @@ -45,11 +45,19 @@ | |||
| 45 | 45 | ||
| 46 | 46 | // Save preferences | |
| 47 | 47 | async function savePreferences() { | |
| 48 | - | if (!onSave) return; | |
| 49 | - | ||
| 50 | 48 | isSaving = true; | |
| 51 | 49 | try { | |
| 52 | - | await onSave(preferences); | |
| 50 | + | const response = await fetch('/api/user/text-preferences', { | |
| 51 | + | method: 'POST', | |
| 52 | + | headers: { 'Content-Type': 'application/json' }, | |
| 53 | + | body: JSON.stringify(preferences) | |
| 54 | + | }); | |
| 55 | + | ||
| 56 | + | if (!response.ok) { | |
| 57 | + | throw new Error('Failed to save preferences'); | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | await onSave?.(preferences); | |
| 53 | 61 | showModal = false; | |
| 54 | 62 | } catch (error) { | |
| 55 | 63 | console.error('Failed to save preferences:', error); | |
| @@ -45,11 +45,19 @@ | |||||
| 45 | 45 | ||||
| 46 | // Save preferences | 46 | // Save preferences | ||
| 47 | async function savePreferences() { | 47 | async function savePreferences() { | ||
| 48 | - | if (!onSave) return; | |||
| 49 | - | ||||
| 50 | isSaving = true; | 48 | isSaving = true; | ||
| 51 | try { | 49 | try { | ||
| 52 | - | await onSave(preferences); | 50 | + | const response = await fetch('/api/user/text-preferences', { |
| 51 | + | method: 'POST', | |||
| 52 | + | headers: { 'Content-Type': 'application/json' }, | |||
| 53 | + | body: JSON.stringify(preferences) | |||
| 54 | + | }); | |||
| 55 | + | ||||
| 56 | + | if (!response.ok) { | |||
| 57 | + | throw new Error('Failed to save preferences'); | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | await onSave?.(preferences); | |||
| 53 | showModal = false; | 61 | showModal = false; | ||
| 54 | } catch (error) { | 62 | } catch (error) { | ||
| 55 | console.error('Failed to save preferences:', error); | 63 | console.error('Failed to save preferences:', error); | ||
M src/lib/db/convex.server.ts +50 -1
| @@ -144,4 +144,53 @@ export interface ConvexChatRoom { | |||
| 144 | 144 | createdAt: number; | |
| 145 | 145 | } | |
| 146 | 146 | ||
| 147 | - | export default { users, sessions, messages, chatRooms }; | |
| 147 | + | export interface ConvexTextPreferences { | |
| 148 | + | _id: string; | |
| 149 | + | _creationTime: number; | |
| 150 | + | userId: string; | |
| 151 | + | defaultStyle: { | |
| 152 | + | fontFamily: string; | |
| 153 | + | fontSize: number; | |
| 154 | + | color?: string; | |
| 155 | + | gradient?: string[]; | |
| 156 | + | bold: boolean; | |
| 157 | + | italic: boolean; | |
| 158 | + | underline: boolean; | |
| 159 | + | strikethrough: boolean; | |
| 160 | + | }; | |
| 161 | + | allowFormatting: boolean; | |
| 162 | + | maxMessageLength: number; | |
| 163 | + | createdAt: number; | |
| 164 | + | updatedAt: number; | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | // ============ TEXT PREFERENCES ============ | |
| 168 | + | ||
| 169 | + | export const textPreferences = { | |
| 170 | + | get: (userId: string) => | |
| 171 | + | convexFetch<ConvexTextPreferences | null>('/api/textPreferences/get', { userId }), | |
| 172 | + | ||
| 173 | + | save: ( | |
| 174 | + | userId: string, | |
| 175 | + | defaultStyle: { | |
| 176 | + | fontFamily: string; | |
| 177 | + | fontSize: number; | |
| 178 | + | color?: string; | |
| 179 | + | gradient?: string[]; | |
| 180 | + | bold: boolean; | |
| 181 | + | italic: boolean; | |
| 182 | + | underline: boolean; | |
| 183 | + | strikethrough: boolean; | |
| 184 | + | }, | |
| 185 | + | allowFormatting: boolean, | |
| 186 | + | maxMessageLength: number | |
| 187 | + | ) => | |
| 188 | + | convexFetch<{ success: boolean; id: string }>('/api/textPreferences/save', { | |
| 189 | + | userId, | |
| 190 | + | defaultStyle, | |
| 191 | + | allowFormatting, | |
| 192 | + | maxMessageLength | |
| 193 | + | }) | |
| 194 | + | }; | |
| 195 | + | ||
| 196 | + | export default { users, sessions, messages, chatRooms, textPreferences }; | |
| @@ -144,4 +144,53 @@ export interface ConvexChatRoom { | |||||
| 144 | createdAt: number; | 144 | createdAt: number; | ||
| 145 | } | 145 | } | ||
| 146 | 146 | ||||
| 147 | - | export default { users, sessions, messages, chatRooms }; | 147 | + | export interface ConvexTextPreferences { |
| 148 | + | _id: string; | |||
| 149 | + | _creationTime: number; | |||
| 150 | + | userId: string; | |||
| 151 | + | defaultStyle: { | |||
| 152 | + | fontFamily: string; | |||
| 153 | + | fontSize: number; | |||
| 154 | + | color?: string; | |||
| 155 | + | gradient?: string[]; | |||
| 156 | + | bold: boolean; | |||
| 157 | + | italic: boolean; | |||
| 158 | + | underline: boolean; | |||
| 159 | + | strikethrough: boolean; | |||
| 160 | + | }; | |||
| 161 | + | allowFormatting: boolean; | |||
| 162 | + | maxMessageLength: number; | |||
| 163 | + | createdAt: number; | |||
| 164 | + | updatedAt: number; | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | // ============ TEXT PREFERENCES ============ | |||
| 168 | + | ||||
| 169 | + | export const textPreferences = { | |||
| 170 | + | get: (userId: string) => | |||
| 171 | + | convexFetch<ConvexTextPreferences | null>('/api/textPreferences/get', { userId }), | |||
| 172 | + | ||||
| 173 | + | save: ( | |||
| 174 | + | userId: string, | |||
| 175 | + | defaultStyle: { | |||
| 176 | + | fontFamily: string; | |||
| 177 | + | fontSize: number; | |||
| 178 | + | color?: string; | |||
| 179 | + | gradient?: string[]; | |||
| 180 | + | bold: boolean; | |||
| 181 | + | italic: boolean; | |||
| 182 | + | underline: boolean; | |||
| 183 | + | strikethrough: boolean; | |||
| 184 | + | }, | |||
| 185 | + | allowFormatting: boolean, | |||
| 186 | + | maxMessageLength: number | |||
| 187 | + | ) => | |||
| 188 | + | convexFetch<{ success: boolean; id: string }>('/api/textPreferences/save', { | |||
| 189 | + | userId, | |||
| 190 | + | defaultStyle, | |||
| 191 | + | allowFormatting, | |||
| 192 | + | maxMessageLength | |||
| 193 | + | }) | |||
| 194 | + | }; | |||
| 195 | + | ||||
| 196 | + | export default { users, sessions, messages, chatRooms, textPreferences }; | |||
M src/routes/+page.server.ts +23 -8
| @@ -1,6 +1,12 @@ | |||
| 1 | 1 | import type { PageServerLoad } from './$types'; | |
| 2 | 2 | import { validateSessionToken } from '$lib/api/session.server'; | |
| 3 | - | import { DEFAULT_TEXT_STYLE, type TextStyle } from '$lib/types/text-formatting'; | |
| 3 | + | import { | |
| 4 | + | DEFAULT_TEXT_STYLE, | |
| 5 | + | DEFAULT_USER_PREFERENCES, | |
| 6 | + | type TextStyle, | |
| 7 | + | type UserTextPreferences | |
| 8 | + | } from '$lib/types/text-formatting'; | |
| 9 | + | import convex from '$lib/db/convex.server'; | |
| 4 | 10 | ||
| 5 | 11 | export const load: PageServerLoad = async ({ cookies }) => { | |
| 6 | 12 | const token = cookies.get('session'); | |
| @@ -8,7 +14,8 @@ export const load: PageServerLoad = async ({ cookies }) => { | |||
| 8 | 14 | if (!token) { | |
| 9 | 15 | return { | |
| 10 | 16 | user: null, | |
| 11 | - | lastTextStyle: DEFAULT_TEXT_STYLE | |
| 17 | + | textStyle: DEFAULT_TEXT_STYLE, | |
| 18 | + | userPreferences: DEFAULT_USER_PREFERENCES | |
| 12 | 19 | }; | |
| 13 | 20 | } | |
| 14 | 21 | ||
| @@ -18,23 +25,31 @@ export const load: PageServerLoad = async ({ cookies }) => { | |||
| 18 | 25 | if (!user) { | |
| 19 | 26 | return { | |
| 20 | 27 | user: null, | |
| 21 | - | lastTextStyle: DEFAULT_TEXT_STYLE | |
| 28 | + | textStyle: DEFAULT_TEXT_STYLE, | |
| 29 | + | userPreferences: DEFAULT_USER_PREFERENCES | |
| 22 | 30 | }; | |
| 23 | 31 | } | |
| 24 | 32 | ||
| 25 | - | // For now, return default text style | |
| 26 | - | // TODO: Fetch user's last text style from Convex if needed | |
| 27 | - | const lastTextStyle: TextStyle = DEFAULT_TEXT_STYLE; | |
| 33 | + | // Fetch user's text preferences from Convex | |
| 34 | + | const prefs = await convex.textPreferences.get(user.id); | |
| 28 | 35 | ||
| 29 | 36 | return { | |
| 30 | 37 | user, | |
| 31 | - | lastTextStyle | |
| 38 | + | textStyle: (prefs?.defaultStyle as TextStyle) ?? DEFAULT_TEXT_STYLE, | |
| 39 | + | userPreferences: prefs | |
| 40 | + | ? { | |
| 41 | + | defaultStyle: prefs.defaultStyle as TextStyle, | |
| 42 | + | allowFormatting: prefs.allowFormatting, | |
| 43 | + | maxMessageLength: prefs.maxMessageLength | |
| 44 | + | } | |
| 45 | + | : DEFAULT_USER_PREFERENCES | |
| 32 | 46 | }; | |
| 33 | 47 | } catch (error) { | |
| 34 | 48 | console.error('Error loading page data:', error); | |
| 35 | 49 | return { | |
| 36 | 50 | user: null, | |
| 37 | - | lastTextStyle: DEFAULT_TEXT_STYLE | |
| 51 | + | textStyle: DEFAULT_TEXT_STYLE, | |
| 52 | + | userPreferences: DEFAULT_USER_PREFERENCES | |
| 38 | 53 | }; | |
| 39 | 54 | } | |
| 40 | 55 | }; | |
| @@ -1,6 +1,12 @@ | |||||
| 1 | import type { PageServerLoad } from './$types'; | 1 | import type { PageServerLoad } from './$types'; | ||
| 2 | import { validateSessionToken } from '$lib/api/session.server'; | 2 | import { validateSessionToken } from '$lib/api/session.server'; | ||
| 3 | - | import { DEFAULT_TEXT_STYLE, type TextStyle } from '$lib/types/text-formatting'; | 3 | + | import { |
| 4 | + | DEFAULT_TEXT_STYLE, | |||
| 5 | + | DEFAULT_USER_PREFERENCES, | |||
| 6 | + | type TextStyle, | |||
| 7 | + | type UserTextPreferences | |||
| 8 | + | } from '$lib/types/text-formatting'; | |||
| 9 | + | import convex from '$lib/db/convex.server'; | |||
| 4 | 10 | ||||
| 5 | export const load: PageServerLoad = async ({ cookies }) => { | 11 | export const load: PageServerLoad = async ({ cookies }) => { | ||
| 6 | const token = cookies.get('session'); | 12 | const token = cookies.get('session'); | ||
| @@ -8,7 +14,8 @@ export const load: PageServerLoad = async ({ cookies }) => { | |||||
| 8 | if (!token) { | 14 | if (!token) { | ||
| 9 | return { | 15 | return { | ||
| 10 | user: null, | 16 | user: null, | ||
| 11 | - | lastTextStyle: DEFAULT_TEXT_STYLE | 17 | + | textStyle: DEFAULT_TEXT_STYLE, |
| 18 | + | userPreferences: DEFAULT_USER_PREFERENCES | |||
| 12 | }; | 19 | }; | ||
| 13 | } | 20 | } | ||
| 14 | 21 | ||||
| @@ -18,23 +25,31 @@ export const load: PageServerLoad = async ({ cookies }) => { | |||||
| 18 | if (!user) { | 25 | if (!user) { | ||
| 19 | return { | 26 | return { | ||
| 20 | user: null, | 27 | user: null, | ||
| 21 | - | lastTextStyle: DEFAULT_TEXT_STYLE | 28 | + | textStyle: DEFAULT_TEXT_STYLE, |
| 29 | + | userPreferences: DEFAULT_USER_PREFERENCES | |||
| 22 | }; | 30 | }; | ||
| 23 | } | 31 | } | ||
| 24 | 32 | ||||
| 25 | - | // For now, return default text style | 33 | + | // Fetch user's text preferences from Convex |
| 26 | - | // TODO: Fetch user's last text style from Convex if needed | 34 | + | const prefs = await convex.textPreferences.get(user.id); |
| 27 | - | const lastTextStyle: TextStyle = DEFAULT_TEXT_STYLE; | |||
| 28 | 35 | ||||
| 29 | return { | 36 | return { | ||
| 30 | user, | 37 | user, | ||
| 31 | - | lastTextStyle | 38 | + | textStyle: (prefs?.defaultStyle as TextStyle) ?? DEFAULT_TEXT_STYLE, |
| 39 | + | userPreferences: prefs | |||
| 40 | + | ? { | |||
| 41 | + | defaultStyle: prefs.defaultStyle as TextStyle, | |||
| 42 | + | allowFormatting: prefs.allowFormatting, | |||
| 43 | + | maxMessageLength: prefs.maxMessageLength | |||
| 44 | + | } | |||
| 45 | + | : DEFAULT_USER_PREFERENCES | |||
| 32 | }; | 46 | }; | ||
| 33 | } catch (error) { | 47 | } catch (error) { | ||
| 34 | console.error('Error loading page data:', error); | 48 | console.error('Error loading page data:', error); | ||
| 35 | return { | 49 | return { | ||
| 36 | user: null, | 50 | user: null, | ||
| 37 | - | lastTextStyle: DEFAULT_TEXT_STYLE | 51 | + | textStyle: DEFAULT_TEXT_STYLE, |
| 52 | + | userPreferences: DEFAULT_USER_PREFERENCES | |||
| 38 | }; | 53 | }; | ||
| 39 | } | 54 | } | ||
| 40 | }; | 55 | }; | ||
M src/routes/+page.svelte +1 -1
| @@ -18,7 +18,7 @@ | |||
| 18 | 18 | <DesktopIcons bind:showChatRoom bind:showAuth /> | |
| 19 | 19 | ||
| 20 | 20 | {#if showChatRoom} | |
| 21 | - | <ChatRoom bind:showChatRoom initialTextStyle={data.lastTextStyle} /> | |
| 21 | + | <ChatRoom bind:showChatRoom initialTextStyle={data.textStyle} /> | |
| 22 | 22 | {/if} | |
| 23 | 23 | ||
| 24 | 24 | {#if showAuth} | |
| @@ -18,7 +18,7 @@ | |||||
| 18 | <DesktopIcons bind:showChatRoom bind:showAuth /> | 18 | <DesktopIcons bind:showChatRoom bind:showAuth /> | ||
| 19 | 19 | ||||
| 20 | {#if showChatRoom} | 20 | {#if showChatRoom} | ||
| 21 | - | <ChatRoom bind:showChatRoom initialTextStyle={data.lastTextStyle} /> | 21 | + | <ChatRoom bind:showChatRoom initialTextStyle={data.textStyle} /> |
| 22 | {/if} | 22 | {/if} | ||
| 23 | 23 | ||||
| 24 | {#if showAuth} | 24 | {#if showAuth} | ||
A src/routes/api/user/text-preferences/+server.ts +102 -0
| @@ -0,0 +1,102 @@ | |||
| 1 | + | import { json, error } from '@sveltejs/kit'; | |
| 2 | + | import type { RequestHandler } from './$types'; | |
| 3 | + | import convex from '$lib/db/convex.server'; | |
| 4 | + | import { DEFAULT_USER_PREFERENCES, type UserTextPreferences } from '$lib/types/text-formatting'; | |
| 5 | + | import { validateSessionToken } from '$lib/api/session.server'; | |
| 6 | + | ||
| 7 | + | // GET - Fetch current user's text preferences | |
| 8 | + | export const GET: RequestHandler = async ({ cookies }) => { | |
| 9 | + | try { | |
| 10 | + | // Get session token from cookie | |
| 11 | + | const sessionToken = cookies.get('pdraim_session'); | |
| 12 | + | if (!sessionToken) { | |
| 13 | + | throw error(401, 'No session found'); | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // Validate session token | |
| 17 | + | const { user } = await validateSessionToken(sessionToken); | |
| 18 | + | ||
| 19 | + | if (!user) { | |
| 20 | + | throw error(401, 'Invalid session'); | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // Get user's text preferences | |
| 24 | + | const preferences = await convex.textPreferences.get(user.id); | |
| 25 | + | ||
| 26 | + | if (!preferences) { | |
| 27 | + | // Return defaults if no preferences exist | |
| 28 | + | return json({ | |
| 29 | + | success: true, | |
| 30 | + | preferences: DEFAULT_USER_PREFERENCES | |
| 31 | + | }); | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // Convert Convex format to frontend format | |
| 35 | + | const frontendPreferences: UserTextPreferences = { | |
| 36 | + | defaultStyle: preferences.defaultStyle as UserTextPreferences['defaultStyle'], | |
| 37 | + | allowFormatting: preferences.allowFormatting, | |
| 38 | + | maxMessageLength: preferences.maxMessageLength | |
| 39 | + | }; | |
| 40 | + | ||
| 41 | + | return json({ | |
| 42 | + | success: true, | |
| 43 | + | preferences: frontendPreferences | |
| 44 | + | }); | |
| 45 | + | } catch (err) { | |
| 46 | + | console.error('Failed to fetch text preferences:', err); | |
| 47 | + | if (err instanceof Response) { | |
| 48 | + | throw err; | |
| 49 | + | } | |
| 50 | + | throw error(500, 'Failed to fetch preferences'); | |
| 51 | + | } | |
| 52 | + | }; | |
| 53 | + | ||
| 54 | + | // POST - Save current user's text preferences | |
| 55 | + | export const POST: RequestHandler = async ({ cookies, request }) => { | |
| 56 | + | try { | |
| 57 | + | // Get session token from cookie | |
| 58 | + | const sessionToken = cookies.get('pdraim_session'); | |
| 59 | + | if (!sessionToken) { | |
| 60 | + | throw error(401, 'No session found'); | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // Validate session token | |
| 64 | + | const { user } = await validateSessionToken(sessionToken); | |
| 65 | + | ||
| 66 | + | if (!user) { | |
| 67 | + | throw error(401, 'Invalid session'); | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // Parse request body | |
| 71 | + | const body = await request.json(); | |
| 72 | + | const { defaultStyle, allowFormatting, maxMessageLength } = body as UserTextPreferences; | |
| 73 | + | ||
| 74 | + | // Validate required fields | |
| 75 | + | if ( | |
| 76 | + | !defaultStyle || | |
| 77 | + | typeof allowFormatting !== 'boolean' || | |
| 78 | + | typeof maxMessageLength !== 'number' | |
| 79 | + | ) { | |
| 80 | + | throw error(400, 'Invalid preferences data'); | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // Save preferences via Convex | |
| 84 | + | const result = await convex.textPreferences.save( | |
| 85 | + | user.id, | |
| 86 | + | defaultStyle, | |
| 87 | + | allowFormatting, | |
| 88 | + | maxMessageLength | |
| 89 | + | ); | |
| 90 | + | ||
| 91 | + | return json({ | |
| 92 | + | success: true, | |
| 93 | + | id: result.id | |
| 94 | + | }); | |
| 95 | + | } catch (err) { | |
| 96 | + | console.error('Failed to save text preferences:', err); | |
| 97 | + | if (err instanceof Response) { | |
| 98 | + | throw err; | |
| 99 | + | } | |
| 100 | + | throw error(500, 'Failed to save preferences'); | |
| 101 | + | } | |
| 102 | + | }; | |
| @@ -0,0 +1,102 @@ | |||||
| 1 | + | import { json, error } from '@sveltejs/kit'; | |||
| 2 | + | import type { RequestHandler } from './$types'; | |||
| 3 | + | import convex from '$lib/db/convex.server'; | |||
| 4 | + | import { DEFAULT_USER_PREFERENCES, type UserTextPreferences } from '$lib/types/text-formatting'; | |||
| 5 | + | import { validateSessionToken } from '$lib/api/session.server'; | |||
| 6 | + | ||||
| 7 | + | // GET - Fetch current user's text preferences | |||
| 8 | + | export const GET: RequestHandler = async ({ cookies }) => { | |||
| 9 | + | try { | |||
| 10 | + | // Get session token from cookie | |||
| 11 | + | const sessionToken = cookies.get('pdraim_session'); | |||
| 12 | + | if (!sessionToken) { | |||
| 13 | + | throw error(401, 'No session found'); | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // Validate session token | |||
| 17 | + | const { user } = await validateSessionToken(sessionToken); | |||
| 18 | + | ||||
| 19 | + | if (!user) { | |||
| 20 | + | throw error(401, 'Invalid session'); | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // Get user's text preferences | |||
| 24 | + | const preferences = await convex.textPreferences.get(user.id); | |||
| 25 | + | ||||
| 26 | + | if (!preferences) { | |||
| 27 | + | // Return defaults if no preferences exist | |||
| 28 | + | return json({ | |||
| 29 | + | success: true, | |||
| 30 | + | preferences: DEFAULT_USER_PREFERENCES | |||
| 31 | + | }); | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // Convert Convex format to frontend format | |||
| 35 | + | const frontendPreferences: UserTextPreferences = { | |||
| 36 | + | defaultStyle: preferences.defaultStyle as UserTextPreferences['defaultStyle'], | |||
| 37 | + | allowFormatting: preferences.allowFormatting, | |||
| 38 | + | maxMessageLength: preferences.maxMessageLength | |||
| 39 | + | }; | |||
| 40 | + | ||||
| 41 | + | return json({ | |||
| 42 | + | success: true, | |||
| 43 | + | preferences: frontendPreferences | |||
| 44 | + | }); | |||
| 45 | + | } catch (err) { | |||
| 46 | + | console.error('Failed to fetch text preferences:', err); | |||
| 47 | + | if (err instanceof Response) { | |||
| 48 | + | throw err; | |||
| 49 | + | } | |||
| 50 | + | throw error(500, 'Failed to fetch preferences'); | |||
| 51 | + | } | |||
| 52 | + | }; | |||
| 53 | + | ||||
| 54 | + | // POST - Save current user's text preferences | |||
| 55 | + | export const POST: RequestHandler = async ({ cookies, request }) => { | |||
| 56 | + | try { | |||
| 57 | + | // Get session token from cookie | |||
| 58 | + | const sessionToken = cookies.get('pdraim_session'); | |||
| 59 | + | if (!sessionToken) { | |||
| 60 | + | throw error(401, 'No session found'); | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // Validate session token | |||
| 64 | + | const { user } = await validateSessionToken(sessionToken); | |||
| 65 | + | ||||
| 66 | + | if (!user) { | |||
| 67 | + | throw error(401, 'Invalid session'); | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // Parse request body | |||
| 71 | + | const body = await request.json(); | |||
| 72 | + | const { defaultStyle, allowFormatting, maxMessageLength } = body as UserTextPreferences; | |||
| 73 | + | ||||
| 74 | + | // Validate required fields | |||
| 75 | + | if ( | |||
| 76 | + | !defaultStyle || | |||
| 77 | + | typeof allowFormatting !== 'boolean' || | |||
| 78 | + | typeof maxMessageLength !== 'number' | |||
| 79 | + | ) { | |||
| 80 | + | throw error(400, 'Invalid preferences data'); | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // Save preferences via Convex | |||
| 84 | + | const result = await convex.textPreferences.save( | |||
| 85 | + | user.id, | |||
| 86 | + | defaultStyle, | |||
| 87 | + | allowFormatting, | |||
| 88 | + | maxMessageLength | |||
| 89 | + | ); | |||
| 90 | + | ||||
| 91 | + | return json({ | |||
| 92 | + | success: true, | |||
| 93 | + | id: result.id | |||
| 94 | + | }); | |||
| 95 | + | } catch (err) { | |||
| 96 | + | console.error('Failed to save text preferences:', err); | |||
| 97 | + | if (err instanceof Response) { | |||
| 98 | + | throw err; | |||
| 99 | + | } | |||
| 100 | + | throw error(500, 'Failed to save preferences'); | |||
| 101 | + | } | |||
| 102 | + | }; | |||