feat: Update and delete an image

This commit is contained in:
wilson 2026-03-17 21:32:47 +00:00
parent 61bf7f6162
commit 933278ec98
52 changed files with 8844 additions and 5755 deletions

View file

@ -6,15 +6,15 @@ module.exports = {
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
'svelte3/typescript': () => require('typescript'),
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
ecmaVersion: 2020,
},
env: {
browser: true,
es2017: true,
node: true
}
node: true,
},
};

View file

@ -36,6 +36,50 @@ export type DateTimeFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | null
notIn?: Date[] | string[] | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
@ -66,6 +110,54 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | null
notIn?: Date[] | string[] | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
@ -88,6 +180,45 @@ export type NestedDateTimeFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | null
notIn?: Date[] | string[] | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
@ -129,4 +260,63 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | null
notIn?: Date[] | string[] | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}

View file

@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
"clientVersion": "7.4.2",
"engineVersion": "94a226be1cf2967af2541cca5529f0f7ba866919",
"activeProvider": "sqlite",
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel PhotoPost {\n id Int @id @default(autoincrement())\n createdAt DateTime @default(now())\n}\n",
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel PhotoPost {\n id Int @id @default(autoincrement())\n createdAt DateTime @default(now())\n deletedAt DateTime?\n publishedAt DateTime?\n filePath String\n fileName String\n title String\n description String?\n}\n",
"runtimeDataModel": {
"models": {},
"enums": {},
@ -32,10 +32,10 @@ const config: runtime.GetPrismaClientConfig = {
}
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"PhotoPost\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
config.runtimeDataModel = JSON.parse("{\"models\":{\"PhotoPost\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"deletedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"publishedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"filePath\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fileName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
config.parameterizationSchema = {
strings: JSON.parse("[\"where\",\"PhotoPost.findUnique\",\"PhotoPost.findUniqueOrThrow\",\"orderBy\",\"cursor\",\"PhotoPost.findFirst\",\"PhotoPost.findFirstOrThrow\",\"PhotoPost.findMany\",\"data\",\"PhotoPost.createOne\",\"PhotoPost.createMany\",\"PhotoPost.createManyAndReturn\",\"PhotoPost.updateOne\",\"PhotoPost.updateMany\",\"PhotoPost.updateManyAndReturn\",\"create\",\"update\",\"PhotoPost.upsertOne\",\"PhotoPost.deleteOne\",\"PhotoPost.deleteMany\",\"having\",\"_count\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"PhotoPost.groupBy\",\"PhotoPost.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"createdAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"set\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"),
graph: "KwsQBRwAACIAMB0AAAQAEB4AACIAMB8CAAAAASBAACQAIQEAAAABACABAAAAAQAgBRwAACIAMB0AAAQAEB4AACIAMB8CACMAISBAACQAIQADAAAABAAgAwAABQAwBAAAAQAgAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACACHwIAAAABIEAAAAABAQgAAAkAIAIfAgAAAAEgQAAAAAEBCAAACwAwAQgAAAsAMAIfAgArACEgQAAqACECAAAAAQAgCAAADgAgAh8CACsAISBAACoAIQIAAAAEACAIAAAQACACAAAABAAgCAAAEAAgAwAAAAEAIA8AAAkAIBAAAA4AIAEAAAABACABAAAABAAgBRUAACUAIBYAACYAIBcAACkAIBgAACgAIBkAACcAIAUcAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEDAAAABAAgAwAAFgAwFAAAFwAgAwAAAAQAIAMAAAUAMAQAAAEAIAUcAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACENFQAAHgAgFgAAIQAgFwAAHgAgGAAAHgAgGQAAHgAgIQIAAAABIgIAAAAEIwIAAAAEJAIAAAABJQIAAAABJgIAAAABJwIAAAABKAIAIAAhCxUAAB4AIBgAAB8AIBkAAB8AICFAAAAAASJAAAAABCNAAAAABCRAAAAAASVAAAAAASZAAAAAASdAAAAAAShAAB0AIQsVAAAeACAYAAAfACAZAAAfACAhQAAAAAEiQAAAAAQjQAAAAAQkQAAAAAElQAAAAAEmQAAAAAEnQAAAAAEoQAAdACEIIQIAAAABIgIAAAAEIwIAAAAEJAIAAAABJQIAAAABJgIAAAABJwIAAAABKAIAHgAhCCFAAAAAASJAAAAABCNAAAAABCRAAAAAASVAAAAAASZAAAAAASdAAAAAAShAAB8AIQ0VAAAeACAWAAAhACAXAAAeACAYAAAeACAZAAAeACAhAgAAAAEiAgAAAAQjAgAAAAQkAgAAAAElAgAAAAEmAgAAAAEnAgAAAAEoAgAgACEIIQgAAAABIggAAAAEIwgAAAAEJAgAAAABJQgAAAABJggAAAABJwgAAAABKAgAIQAhBRwAACIAMB0AAAQAEB4AACIAMB8CACMAISBAACQAIQghAgAAAAEiAgAAAAQjAgAAAAQkAgAAAAElAgAAAAEmAgAAAAEnAgAAAAEoAgAeACEIIUAAAAABIkAAAAAEI0AAAAAEJEAAAAABJUAAAAABJkAAAAABJ0AAAAABKEAAHwAhAAAAAAABKUAAAAABBSkCAAAAASoCAAAAASsCAAAAASwCAAAAAS0CAAAAAQAAAAAFFQAGFgAHFwAIGAAJGQAKAAAAAAAFFQAGFgAHFwAIGAAJGQAKAQIBAgMBBQYBBgcBBwgBCQoBCgwCCw0DDA8BDRECDhIEERMBEhQBExUCGhgFGxkL"
strings: JSON.parse("[\"where\",\"PhotoPost.findUnique\",\"PhotoPost.findUniqueOrThrow\",\"orderBy\",\"cursor\",\"PhotoPost.findFirst\",\"PhotoPost.findFirstOrThrow\",\"PhotoPost.findMany\",\"data\",\"PhotoPost.createOne\",\"PhotoPost.createMany\",\"PhotoPost.createManyAndReturn\",\"PhotoPost.updateOne\",\"PhotoPost.updateMany\",\"PhotoPost.updateManyAndReturn\",\"create\",\"update\",\"PhotoPost.upsertOne\",\"PhotoPost.deleteOne\",\"PhotoPost.deleteMany\",\"having\",\"_count\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"PhotoPost.groupBy\",\"PhotoPost.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"createdAt\",\"deletedAt\",\"publishedAt\",\"filePath\",\"fileName\",\"title\",\"description\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"contains\",\"startsWith\",\"endsWith\",\"not\",\"set\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"),
graph: "PAsQCxwAACwAMB0AAAQAEB4AACwAMB8CAAAAASBAAC4AISFAAC8AISJAAC8AISMBADAAISQBADAAISUBADAAISYBADEAIQEAAAABACABAAAAAQAgCxwAACwAMB0AAAQAEB4AACwAMB8CAC0AISBAAC4AISFAAC8AISJAAC8AISMBADAAISQBADAAISUBADAAISYBADEAIQMhAAAyACAiAAAyACAmAAAyACADAAAABAAgAwAABQAwBAAAAQAgAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACAIHwIAAAABIEAAAAABIUAAAAABIkAAAAABIwEAAAABJAEAAAABJQEAAAABJgEAAAABAQgAAAkAIAgfAgAAAAEgQAAAAAEhQAAAAAEiQAAAAAEjAQAAAAEkAQAAAAElAQAAAAEmAQAAAAEBCAAACwAwAQgAAAsAMAgfAgA8ACEgQAA4ACEhQAA5ACEiQAA5ACEjAQA6ACEkAQA6ACElAQA6ACEmAQA7ACECAAAAAQAgCAAADgAgCB8CADwAISBAADgAISFAADkAISJAADkAISMBADoAISQBADoAISUBADoAISYBADsAIQIAAAAEACAIAAAQACACAAAABAAgCAAAEAAgAwAAAAEAIA8AAAkAIBAAAA4AIAEAAAABACABAAAABAAgCBUAADMAIBYAADQAIBcAADcAIBgAADYAIBkAADUAICEAADIAICIAADIAICYAADIAIAscAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEhQAAdACEiQAAdACEjAQAeACEkAQAeACElAQAeACEmAQAfACEDAAAABAAgAwAAFgAwFAAAFwAgAwAAAAQAIAMAAAUAMAQAAAEAIAscAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEhQAAdACEiQAAdACEjAQAeACEkAQAeACElAQAeACEmAQAfACENFQAAJAAgFgAAKwAgFwAAJAAgGAAAJAAgGQAAJAAgJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAKgAhCxUAACQAIBgAACkAIBkAACkAICdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACgAIQsVAAAhACAYAAAnACAZAAAnACAnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAmACEOFQAAJAAgGAAAJQAgGQAAJQAgJwEAAAABKAEAAAAEKQEAAAAEKgEAAAABKwEAAAABLAEAAAABLQEAAAABLgEAAAABLwEAAAABMAEAAAABMQEAIwAhDhUAACEAIBgAACIAIBkAACIAICcBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACAAIQ4VAAAhACAYAAAiACAZAAAiACAnAQAAAAEoAQAAAAUpAQAAAAUqAQAAAAErAQAAAAEsAQAAAAEtAQAAAAEuAQAAAAEvAQAAAAEwAQAAAAExAQAgACEIJwIAAAABKAIAAAAFKQIAAAAFKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAIQAhCycBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACIAIQ4VAAAkACAYAAAlACAZAAAlACAnAQAAAAEoAQAAAAQpAQAAAAQqAQAAAAErAQAAAAEsAQAAAAEtAQAAAAEuAQAAAAEvAQAAAAEwAQAAAAExAQAjACEIJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAJAAhCycBAAAAASgBAAAABCkBAAAABCoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACUAIQsVAAAhACAYAAAnACAZAAAnACAnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAmACEIJ0AAAAABKEAAAAAFKUAAAAAFKkAAAAABK0AAAAABLEAAAAABLUAAAAABMUAAJwAhCxUAACQAIBgAACkAIBkAACkAICdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACgAIQgnQAAAAAEoQAAAAAQpQAAAAAQqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAApACENFQAAJAAgFgAAKwAgFwAAJAAgGAAAJAAgGQAAJAAgJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAKgAhCCcIAAAAASgIAAAABCkIAAAABCoIAAAAASsIAAAAASwIAAAAAS0IAAAAATEIACsAIQscAAAsADAdAAAEABAeAAAsADAfAgAtACEgQAAuACEhQAAvACEiQAAvACEjAQAwACEkAQAwACElAQAwACEmAQAxACEIJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAJAAhCCdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACkAIQgnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAnACELJwEAAAABKAEAAAAEKQEAAAAEKgEAAAABKwEAAAABLAEAAAABLQEAAAABLgEAAAABLwEAAAABMAEAAAABMQEAJQAhCycBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACIAIQAAAAAAAAEyQAAAAAEBMkAAAAABATIBAAAAAQEyAQAAAAEFMgIAAAABMwIAAAABNAIAAAABNQIAAAABNgIAAAABAAAAAAUVAAYWAAcXAAgYAAkZAAoAAAAAAAUVAAYWAAcXAAgYAAkZAAoBAgECAwEFBgEGBwEHCAEJCgEKDAILDQMMDwENEQIOEgQREwESFAETFQIaGAUbGQs"
}
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {

View file

@ -516,7 +516,13 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
export const PhotoPostScalarFieldEnum = {
id: 'id',
createdAt: 'createdAt'
createdAt: 'createdAt',
deletedAt: 'deletedAt',
publishedAt: 'publishedAt',
filePath: 'filePath',
fileName: 'fileName',
title: 'title',
description: 'description'
} as const
export type PhotoPostScalarFieldEnum = (typeof PhotoPostScalarFieldEnum)[keyof typeof PhotoPostScalarFieldEnum]
@ -530,6 +536,14 @@ export const SortOrder = {
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
/**
* Field references
@ -550,6 +564,13 @@ export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel
/**
* Reference to a field of type 'String'
*/
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
/**
* Reference to a field of type 'Float'
*/

View file

@ -69,7 +69,13 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
export const PhotoPostScalarFieldEnum = {
id: 'id',
createdAt: 'createdAt'
createdAt: 'createdAt',
deletedAt: 'deletedAt',
publishedAt: 'publishedAt',
filePath: 'filePath',
fileName: 'fileName',
title: 'title',
description: 'description'
} as const
export type PhotoPostScalarFieldEnum = (typeof PhotoPostScalarFieldEnum)[keyof typeof PhotoPostScalarFieldEnum]
@ -82,3 +88,11 @@ export const SortOrder = {
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]

View file

@ -37,16 +37,34 @@ export type PhotoPostSumAggregateOutputType = {
export type PhotoPostMinAggregateOutputType = {
id: number | null
createdAt: Date | null
deletedAt: Date | null
publishedAt: Date | null
filePath: string | null
fileName: string | null
title: string | null
description: string | null
}
export type PhotoPostMaxAggregateOutputType = {
id: number | null
createdAt: Date | null
deletedAt: Date | null
publishedAt: Date | null
filePath: string | null
fileName: string | null
title: string | null
description: string | null
}
export type PhotoPostCountAggregateOutputType = {
id: number
createdAt: number
deletedAt: number
publishedAt: number
filePath: number
fileName: number
title: number
description: number
_all: number
}
@ -62,16 +80,34 @@ export type PhotoPostSumAggregateInputType = {
export type PhotoPostMinAggregateInputType = {
id?: true
createdAt?: true
deletedAt?: true
publishedAt?: true
filePath?: true
fileName?: true
title?: true
description?: true
}
export type PhotoPostMaxAggregateInputType = {
id?: true
createdAt?: true
deletedAt?: true
publishedAt?: true
filePath?: true
fileName?: true
title?: true
description?: true
}
export type PhotoPostCountAggregateInputType = {
id?: true
createdAt?: true
deletedAt?: true
publishedAt?: true
filePath?: true
fileName?: true
title?: true
description?: true
_all?: true
}
@ -164,6 +200,12 @@ export type PhotoPostGroupByArgs<ExtArgs extends runtime.Types.Extensions.Intern
export type PhotoPostGroupByOutputType = {
id: number
createdAt: Date
deletedAt: Date | null
publishedAt: Date | null
filePath: string
fileName: string
title: string
description: string | null
_count: PhotoPostCountAggregateOutputType | null
_avg: PhotoPostAvgAggregateOutputType | null
_sum: PhotoPostSumAggregateOutputType | null
@ -192,11 +234,23 @@ export type PhotoPostWhereInput = {
NOT?: Prisma.PhotoPostWhereInput | Prisma.PhotoPostWhereInput[]
id?: Prisma.IntFilter<"PhotoPost"> | number
createdAt?: Prisma.DateTimeFilter<"PhotoPost"> | Date | string
deletedAt?: Prisma.DateTimeNullableFilter<"PhotoPost"> | Date | string | null
publishedAt?: Prisma.DateTimeNullableFilter<"PhotoPost"> | Date | string | null
filePath?: Prisma.StringFilter<"PhotoPost"> | string
fileName?: Prisma.StringFilter<"PhotoPost"> | string
title?: Prisma.StringFilter<"PhotoPost"> | string
description?: Prisma.StringNullableFilter<"PhotoPost"> | string | null
}
export type PhotoPostOrderByWithRelationInput = {
id?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
deletedAt?: Prisma.SortOrderInput | Prisma.SortOrder
publishedAt?: Prisma.SortOrderInput | Prisma.SortOrder
filePath?: Prisma.SortOrder
fileName?: Prisma.SortOrder
title?: Prisma.SortOrder
description?: Prisma.SortOrderInput | Prisma.SortOrder
}
export type PhotoPostWhereUniqueInput = Prisma.AtLeast<{
@ -205,11 +259,23 @@ export type PhotoPostWhereUniqueInput = Prisma.AtLeast<{
OR?: Prisma.PhotoPostWhereInput[]
NOT?: Prisma.PhotoPostWhereInput | Prisma.PhotoPostWhereInput[]
createdAt?: Prisma.DateTimeFilter<"PhotoPost"> | Date | string
deletedAt?: Prisma.DateTimeNullableFilter<"PhotoPost"> | Date | string | null
publishedAt?: Prisma.DateTimeNullableFilter<"PhotoPost"> | Date | string | null
filePath?: Prisma.StringFilter<"PhotoPost"> | string
fileName?: Prisma.StringFilter<"PhotoPost"> | string
title?: Prisma.StringFilter<"PhotoPost"> | string
description?: Prisma.StringNullableFilter<"PhotoPost"> | string | null
}, "id">
export type PhotoPostOrderByWithAggregationInput = {
id?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
deletedAt?: Prisma.SortOrderInput | Prisma.SortOrder
publishedAt?: Prisma.SortOrderInput | Prisma.SortOrder
filePath?: Prisma.SortOrder
fileName?: Prisma.SortOrder
title?: Prisma.SortOrder
description?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.PhotoPostCountOrderByAggregateInput
_avg?: Prisma.PhotoPostAvgOrderByAggregateInput
_max?: Prisma.PhotoPostMaxOrderByAggregateInput
@ -223,43 +289,97 @@ export type PhotoPostScalarWhereWithAggregatesInput = {
NOT?: Prisma.PhotoPostScalarWhereWithAggregatesInput | Prisma.PhotoPostScalarWhereWithAggregatesInput[]
id?: Prisma.IntWithAggregatesFilter<"PhotoPost"> | number
createdAt?: Prisma.DateTimeWithAggregatesFilter<"PhotoPost"> | Date | string
deletedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"PhotoPost"> | Date | string | null
publishedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"PhotoPost"> | Date | string | null
filePath?: Prisma.StringWithAggregatesFilter<"PhotoPost"> | string
fileName?: Prisma.StringWithAggregatesFilter<"PhotoPost"> | string
title?: Prisma.StringWithAggregatesFilter<"PhotoPost"> | string
description?: Prisma.StringNullableWithAggregatesFilter<"PhotoPost"> | string | null
}
export type PhotoPostCreateInput = {
createdAt?: Date | string
deletedAt?: Date | string | null
publishedAt?: Date | string | null
filePath: string
fileName: string
title: string
description?: string | null
}
export type PhotoPostUncheckedCreateInput = {
id?: number
createdAt?: Date | string
deletedAt?: Date | string | null
publishedAt?: Date | string | null
filePath: string
fileName: string
title: string
description?: string | null
}
export type PhotoPostUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
publishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
filePath?: Prisma.StringFieldUpdateOperationsInput | string
fileName?: Prisma.StringFieldUpdateOperationsInput | string
title?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type PhotoPostUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
publishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
filePath?: Prisma.StringFieldUpdateOperationsInput | string
fileName?: Prisma.StringFieldUpdateOperationsInput | string
title?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type PhotoPostCreateManyInput = {
id?: number
createdAt?: Date | string
deletedAt?: Date | string | null
publishedAt?: Date | string | null
filePath: string
fileName: string
title: string
description?: string | null
}
export type PhotoPostUpdateManyMutationInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
publishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
filePath?: Prisma.StringFieldUpdateOperationsInput | string
fileName?: Prisma.StringFieldUpdateOperationsInput | string
title?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type PhotoPostUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
publishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
filePath?: Prisma.StringFieldUpdateOperationsInput | string
fileName?: Prisma.StringFieldUpdateOperationsInput | string
title?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type PhotoPostCountOrderByAggregateInput = {
id?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
deletedAt?: Prisma.SortOrder
publishedAt?: Prisma.SortOrder
filePath?: Prisma.SortOrder
fileName?: Prisma.SortOrder
title?: Prisma.SortOrder
description?: Prisma.SortOrder
}
export type PhotoPostAvgOrderByAggregateInput = {
@ -269,11 +389,23 @@ export type PhotoPostAvgOrderByAggregateInput = {
export type PhotoPostMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
deletedAt?: Prisma.SortOrder
publishedAt?: Prisma.SortOrder
filePath?: Prisma.SortOrder
fileName?: Prisma.SortOrder
title?: Prisma.SortOrder
description?: Prisma.SortOrder
}
export type PhotoPostMinOrderByAggregateInput = {
id?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
deletedAt?: Prisma.SortOrder
publishedAt?: Prisma.SortOrder
filePath?: Prisma.SortOrder
fileName?: Prisma.SortOrder
title?: Prisma.SortOrder
description?: Prisma.SortOrder
}
export type PhotoPostSumOrderByAggregateInput = {
@ -284,6 +416,18 @@ export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string
}
export type NullableDateTimeFieldUpdateOperationsInput = {
set?: Date | string | null
}
export type StringFieldUpdateOperationsInput = {
set?: string
}
export type NullableStringFieldUpdateOperationsInput = {
set?: string | null
}
export type IntFieldUpdateOperationsInput = {
set?: number
increment?: number
@ -297,24 +441,48 @@ export type IntFieldUpdateOperationsInput = {
export type PhotoPostSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
createdAt?: boolean
deletedAt?: boolean
publishedAt?: boolean
filePath?: boolean
fileName?: boolean
title?: boolean
description?: boolean
}, ExtArgs["result"]["photoPost"]>
export type PhotoPostSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
createdAt?: boolean
deletedAt?: boolean
publishedAt?: boolean
filePath?: boolean
fileName?: boolean
title?: boolean
description?: boolean
}, ExtArgs["result"]["photoPost"]>
export type PhotoPostSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
createdAt?: boolean
deletedAt?: boolean
publishedAt?: boolean
filePath?: boolean
fileName?: boolean
title?: boolean
description?: boolean
}, ExtArgs["result"]["photoPost"]>
export type PhotoPostSelectScalar = {
id?: boolean
createdAt?: boolean
deletedAt?: boolean
publishedAt?: boolean
filePath?: boolean
fileName?: boolean
title?: boolean
description?: boolean
}
export type PhotoPostOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "createdAt", ExtArgs["result"]["photoPost"]>
export type PhotoPostOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "createdAt" | "deletedAt" | "publishedAt" | "filePath" | "fileName" | "title" | "description", ExtArgs["result"]["photoPost"]>
export type $PhotoPostPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "PhotoPost"
@ -322,6 +490,12 @@ export type $PhotoPostPayload<ExtArgs extends runtime.Types.Extensions.InternalA
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: number
createdAt: Date
deletedAt: Date | null
publishedAt: Date | null
filePath: string
fileName: string
title: string
description: string | null
}, ExtArgs["result"]["photoPost"]>
composites: {}
}
@ -747,6 +921,12 @@ export interface Prisma__PhotoPostClient<T, Null = never, ExtArgs extends runtim
export interface PhotoPostFieldRefs {
readonly id: Prisma.FieldRef<"PhotoPost", 'Int'>
readonly createdAt: Prisma.FieldRef<"PhotoPost", 'DateTime'>
readonly deletedAt: Prisma.FieldRef<"PhotoPost", 'DateTime'>
readonly publishedAt: Prisma.FieldRef<"PhotoPost", 'DateTime'>
readonly filePath: Prisma.FieldRef<"PhotoPost", 'String'>
readonly fileName: Prisma.FieldRef<"PhotoPost", 'String'>
readonly title: Prisma.FieldRef<"PhotoPost", 'String'>
readonly description: Prisma.FieldRef<"PhotoPost", 'String'>
}
@ -941,7 +1121,7 @@ export type PhotoPostCreateArgs<ExtArgs extends runtime.Types.Extensions.Interna
/**
* The data needed to create a PhotoPost.
*/
data?: Prisma.XOR<Prisma.PhotoPostCreateInput, Prisma.PhotoPostUncheckedCreateInput>
data: Prisma.XOR<Prisma.PhotoPostCreateInput, Prisma.PhotoPostUncheckedCreateInput>
}
/**

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: "./prisma/schema.prisma",
schema: './prisma/schema.prisma',
migrations: {
path: "./prisma/migrations",
path: './prisma/migrations',
},
datasource: {
url: process.env["DATABASE_URL"],
url: process.env['DATABASE_URL'],
},
});

View file

@ -0,0 +1,26 @@
/*
Warnings:
- Added the required column `fileName` to the `PhotoPost` table without a default value. This is not possible if the table is not empty.
- Added the required column `filePath` to the `PhotoPost` table without a default value. This is not possible if the table is not empty.
- Added the required column `title` to the `PhotoPost` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_PhotoPost" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deletedAt" DATETIME,
"publishedAt" DATETIME,
"filePath" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT
);
INSERT INTO "new_PhotoPost" ("createdAt", "id") SELECT "createdAt", "id" FROM "PhotoPost";
DROP TABLE "PhotoPost";
ALTER TABLE "new_PhotoPost" RENAME TO "PhotoPost";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View file

@ -10,4 +10,10 @@ datasource db {
model PhotoPost {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
deletedAt DateTime?
publishedAt DateTime?
filePath String
fileName String
title String
description String?
}

View file

@ -44,13 +44,13 @@
method="POST"
action="/admin/photos"
enctype="multipart/form-data"
class="admin-form"
class="cms-form"
>
<canvas bind:this={canvasElement}></canvas>
<div class="field">
<label for="title">Title</label>
<input type="text" name="title" id="title" />
<input type="text" name="title" id="title" required />
</div>
<div class="field">
@ -59,6 +59,7 @@
type="file"
name="file"
accept="image/*"
required
/>
</div>

View file

@ -0,0 +1,52 @@
import { randomUUID } from "node:crypto";
import { unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
/**
*
*/
export class LocalFileRepository {
constructor(private readonly uploadDirectory: string) {}
private log(message: string): void {
console.log(`[LocalFileRepository] ${message}`);
}
async saveFile(file: File): Promise<{
fileName: string;
filePath: string;
}> {
this.log(
`Saving file: ${file.name} (size: ${file.size} bytes, type: ${file.type})`,
);
const filetype = file.type.split("/")[1];
const fileName = `${randomUUID()}.${filetype}`;
const filePath = join(this.uploadDirectory, fileName);
const fileContentBuffer = await file.arrayBuffer();
await writeFile(filePath, Buffer.from(fileContentBuffer));
this.log(`Successfully saved file to: ${filePath}`);
return {
fileName,
filePath,
};
}
async deleteFile(filePath: string): Promise<void> {
this.log(`Attempting to delete file at path: ${filePath}`);
try {
await unlink(filePath);
this.log(`Successfully deleted file at path: ${filePath}`);
} catch (error: unknown) {
// Treat missing files as already deleted so admin actions remain idempotent.
if (
!(error instanceof Error) ||
!("code" in error) ||
error.code !== "ENOENT"
) {
throw error;
}
}
}
}

View file

@ -1,18 +1,7 @@
import {
describe,
it,
beforeEach,
afterAll,
beforeAll,
expect,
afterEach,
} from "vitest";
import { BlogController } from "./BlogController.js";
import { MarkdownRepository } from "./markdown-repository.js";
import {
exampleMarkdown,
exampleMarkdownFrontmatter,
} from "./test-fixtures/example-markdown.js";
import { describe, it, beforeEach, afterAll, beforeAll, expect, afterEach } from 'vitest';
import { BlogController } from './BlogController.js';
import { MarkdownRepository } from './markdown-repository.js';
import { exampleMarkdown, exampleMarkdownFrontmatter } from './test-fixtures/example-markdown.js';
describe(`BlogController`, () => {
let controller: BlogController;
@ -27,13 +16,9 @@ describe(`BlogController`, () => {
const blogPosts = await controller.getAllBlogPosts();
// WHEN
const aKnownBlogPost = blogPosts.find(
(post) => post.title === "Vibe Check #10",
);
const aKnownBookReview = blogPosts.find((post) => post.title === "After");
const aMadeUpBlogPost = blogPosts.find(
(post) => post.title === "Some made up blog post",
);
const aKnownBlogPost = blogPosts.find((post) => post.title === 'Vibe Check #10');
const aKnownBookReview = blogPosts.find((post) => post.title === 'After');
const aMadeUpBlogPost = blogPosts.find((post) => post.title === 'Some made up blog post');
// then
expect(aMadeUpBlogPost).toBeUndefined();
@ -45,9 +30,7 @@ describe(`BlogController`, () => {
describe(`getBlogPostBySlug`, () => {
it(`should return null when the post doesn't exist`, async () => {
// When
const shouldBeNull = await controller.getBlogPostBySlug(
"some-made-up-blog-post",
);
const shouldBeNull = await controller.getBlogPostBySlug('some-made-up-blog-post');
// Then
expect(shouldBeNull).toBeNull();
@ -55,26 +38,23 @@ describe(`BlogController`, () => {
it(`should return the blog post when it exists`, async () => {
// When
const blogPost = await controller.getBlogPostBySlug(
"2023-02-03-vibe-check-10",
);
const blogPost = await controller.getBlogPostBySlug('2023-02-03-vibe-check-10');
// Then
expect(blogPost).not.toBeNull();
expect(blogPost.title).toBe("Vibe Check #10");
expect(blogPost.title).toBe('Vibe Check #10');
});
});
describe(`Finding content by slug`, () => {
describe(`Finding a blog post`, () => {
// GIVEN
const slugForRealBlogPost = "2023-02-03-vibe-check-10";
const slugForFakeBlogPost = "some-made-up-blog-post";
const slugForRealBlogPost = '2023-02-03-vibe-check-10';
const slugForFakeBlogPost = 'some-made-up-blog-post';
it(`should return null if there's no blog post with the slug`, async () => {
// WHEN
const blogPost =
await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost);
const blogPost = await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost);
// THEN
expect(blogPost).toBeNull();
@ -82,18 +62,17 @@ describe(`BlogController`, () => {
it(`should return the blog post if it exists`, async () => {
// WHEN
const blogPost =
await controller.getAnyKindOfContentBySlug(slugForRealBlogPost);
const blogPost = await controller.getAnyKindOfContentBySlug(slugForRealBlogPost);
// THEN
expect(blogPost).not.toBeNull();
expect(blogPost.title).toBe("Vibe Check #10");
expect(blogPost.title).toBe('Vibe Check #10');
});
});
describe(`Finding a book review`, () => {
const realSlug = "after";
const fakeSlug = "some-made-up-book-review";
const realSlug = 'after';
const fakeSlug = 'some-made-up-book-review';
it(`should return null if there's no book review with the slug`, async () => {
// WHEN
@ -109,17 +88,17 @@ describe(`BlogController`, () => {
// THEN
expect(bookReview).not.toBeNull();
expect(bookReview.title).toBe("After");
expect(bookReview.title).toBe('After');
});
});
});
describe(`Creating a new blog post as a file`, () => {
const thisDirectory = import.meta.url
.replace("file://", "")
.split("/")
.filter((part) => part !== "BlogController.test.ts")
.join("/");
.replace('file://', '')
.split('/')
.filter((part) => part !== 'BlogController.test.ts')
.join('/');
const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`;
let controller: BlogController;
@ -136,10 +115,7 @@ describe(`BlogController`, () => {
const markdownContent = exampleMarkdown;
// WHEN
const blogPost = await controller.createBlogPost(
fileName,
markdownContent,
);
const blogPost = await controller.createBlogPost(fileName, markdownContent);
// THEN
expect(blogPost).not.toBeNull();

View file

@ -1,13 +1,13 @@
import type { BlogPost } from "./BlogPost.js";
import type { BookReview } from "./BookReview.js";
import { MarkdownRepository } from "./markdown-repository.js";
import type { BlogPost } from './BlogPost.js';
import type { BookReview } from './BookReview.js';
import { MarkdownRepository } from './markdown-repository.js';
export interface BlogItem {
title: string;
date: string;
content: string;
slug: string;
content_type: "blog" | "book_review" | "snout_street_studios";
content_type: 'blog' | 'book_review' | 'snout_street_studios';
tags?: string[];
}
@ -46,40 +46,29 @@ export class BlogController {
return this._markdownRepository;
}
async createBlogPost(
resolvedFileName: string,
markdownContent: string,
): Promise<BlogPost> {
const createdBlogPost =
await this._markdownRepository.createBlogPostMarkdownFile(
async createBlogPost(resolvedFileName: string, markdownContent: string): Promise<BlogPost> {
const createdBlogPost = await this._markdownRepository.createBlogPostMarkdownFile(
resolvedFileName,
markdownContent,
markdownContent
);
this._markdownRepository = await MarkdownRepository.singleton(true);
return createdBlogPost;
}
async getAllBlogPosts(
pageSize?: number,
): Promise<Array<BlogPostListItem | BookReviewListItem>> {
async getAllBlogPosts(pageSize?: number): Promise<Array<BlogPostListItem | BookReviewListItem>> {
const blogPosts = this._markdownRepository.blogPosts;
const bookReviews = this._markdownRepository.bookReviews;
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map(
(blogPost) => {
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => {
return this.blogPostToBlogPostListItem(blogPost);
},
);
});
const bookReviewListItems: BookReviewListItem[] =
bookReviews.bookReviews.map((bookReview) => {
const bookReviewListItems: BookReviewListItem[] = bookReviews.bookReviews.map((bookReview) => {
return this.bookReviewToBookReviewListItem(bookReview);
});
const allBlogPosts = [...blogPostListItems, ...bookReviewListItems].sort(
(a, b) => (a.date > b.date ? -1 : 1),
);
const allBlogPosts = [...blogPostListItems, ...bookReviewListItems].sort((a, b) => (a.date > b.date ? -1 : 1));
if (pageSize === undefined) {
return allBlogPosts;
@ -99,19 +88,13 @@ export class BlogController {
async getBlogPostsByTags(tags: string[]): Promise<BlogPostListItem[]> {
const posts = await this.getAllBlogPosts();
const blogPosts = posts.filter(
(post) => post.content_type === "blog",
) as BlogPostListItem[];
const blogPosts = posts.filter((post) => post.content_type === 'blog') as BlogPostListItem[];
return blogPosts
.filter((post: BlogPostListItem) => post["tags"]?.length > 0)
.filter((post: BlogPostListItem) =>
(post.tags as string[]).some((tag) => tags.includes(tag)),
);
.filter((post: BlogPostListItem) => post['tags']?.length > 0)
.filter((post: BlogPostListItem) => (post.tags as string[]).some((tag) => tags.includes(tag)));
}
async getAnyKindOfContentBySlug(
slug: string,
): Promise<BookReviewListItem | BlogPostListItem | null> {
async getAnyKindOfContentBySlug(slug: string): Promise<BookReviewListItem | BlogPostListItem | null> {
const blogPost = this._markdownRepository.getBlogPostBySlug(slug);
if (blogPost) {
return this.blogPostToBlogPostListItem(blogPost);
@ -125,9 +108,7 @@ export class BlogController {
return null;
}
private bookReviewToBookReviewListItem(
bookReview: BookReview,
): BookReviewListItem {
private bookReviewToBookReviewListItem(bookReview: BookReview): BookReviewListItem {
return {
book_review: true,
title: bookReview.title,
@ -138,7 +119,7 @@ export class BlogController {
score: bookReview.score,
slug: bookReview.slug,
content: bookReview.html,
content_type: "book_review",
content_type: 'book_review',
};
}
@ -151,7 +132,7 @@ export class BlogController {
date: blogPost.date.toISOString(),
preview: blogPost.excerpt,
slug: blogPost.slug,
content_type: "blog",
content_type: 'blog',
tags: blogPost.tags,
};
}

View file

@ -8,7 +8,10 @@ import type { BookReview } from './BookReview.js';
export class RssFeed {
private feed: Feed;
constructor(private readonly blogPosts: BlogPostSet, private readonly bookReviews: BookReviewSet) {
constructor(
private readonly blogPosts: BlogPostSet,
private readonly bookReviews: BookReviewSet
) {
this.feed = new Feed({
copyright: `All Rights Reserved Thomas Wilson 2023`,
id: 'https://www.thomaswilson.xyz',

View file

@ -1,14 +1,14 @@
import type { Cookies } from "@sveltejs/kit";
import type { Cookies } from '@sveltejs/kit';
export class CookieAuthentication {
private readonly cookieValue: string;
private readonly cookieValueArray: string[];
public static cookieName = "auth";
public static adminAuthRole = "admin";
public static cookieName = 'auth';
public static adminAuthRole = 'admin';
constructor(private readonly cookies: Cookies) {
this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? "";
this.cookieValueArray = this.cookieValue.split(",");
this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? '';
this.cookieValueArray = this.cookieValue.split(',');
}
public get isAuthdAsAdmin(): boolean {
@ -24,22 +24,18 @@ export class CookieAuthentication {
public logout() {
if (!this.isAuthdAsAdmin) return;
this.cookies.delete(CookieAuthentication.cookieName, { path: "/" });
this.cookies.delete(CookieAuthentication.cookieName, { path: '/' });
}
public setAdminAuthentication(isAuthd: boolean) {
let value = this.cookieValue;
if (isAuthd) {
value = Array.from(
new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole]),
).join(",");
value = Array.from(new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole])).join(',');
} else {
value = this.cookieValueArray
.filter((i) => i !== CookieAuthentication.adminAuthRole)
.join(",");
value = this.cookieValueArray.filter((i) => i !== CookieAuthentication.adminAuthRole).join(',');
}
this.cookies.set(CookieAuthentication.cookieName, value, { path: "/" });
this.cookies.set(CookieAuthentication.cookieName, value, { path: '/' });
}
}

View file

@ -1,19 +1,16 @@
import { describe, it, expect, beforeAll, beforeEach } from "vitest";
import { MarkdownRepository } from "./markdown-repository.js";
import { resolve, dirname } from "path";
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { MarkdownRepository } from './markdown-repository.js';
import { resolve, dirname } from 'path';
import { aBlogPost } from "./test-builders/blog-post-builder.js";
import { aBlogPost } from './test-builders/blog-post-builder.js';
const blogPostImport = import.meta.glob(`./test-fixtures/blog-*.md`, {
as: "raw",
as: 'raw',
});
const bookReviewImport = import.meta.glob(`./test-fixtures/book-review-*.md`, {
as: "raw",
as: 'raw',
});
const snoutStreetPostImport = import.meta.glob(
`./test-fixtures/snout-street-studio-*.md`,
{ as: "raw" },
);
const snoutStreetPostImport = import.meta.glob(`./test-fixtures/snout-street-studio-*.md`, { as: 'raw' });
const expectedHtml = `<p>This is a blog post written in markdown.</p>
<p>This is a <a href="http://www.bbc.co.uk">link</a></p>`;
@ -22,27 +19,23 @@ describe(`Blog MarkdownRepository`, () => {
let repository: MarkdownRepository;
beforeEach(async () => {
repository = await MarkdownRepository.fromViteGlobImport(
blogPostImport,
bookReviewImport,
);
repository = await MarkdownRepository.fromViteGlobImport(blogPostImport, bookReviewImport);
});
it(`should load`, async () => {
// GIVEN
const expectedBlogPost = aBlogPost()
.withAuthor("Thomas Wilson")
.withDate(new Date("2023-02-01T08:00:00Z"))
.withSlug("2023-02-01-test")
.withTitle("Test Blog Post")
.withExcerpt("This is a blog post written in markdown. This is a link")
.withAuthor('Thomas Wilson')
.withDate(new Date('2023-02-01T08:00:00Z'))
.withSlug('2023-02-01-test')
.withTitle('Test Blog Post')
.withExcerpt('This is a blog post written in markdown. This is a link')
.withHtml(expectedHtml)
.withFileName("blog-2023-02-01-test.md")
.withFileName('blog-2023-02-01-test.md')
.build();
// WHEN
const blogPost =
repository.blogPosts.getBlogPostWithTitle("Test Blog Post");
const blogPost = repository.blogPosts.getBlogPostWithTitle('Test Blog Post');
// THEN
expect(repository).toBeDefined();
@ -58,7 +51,7 @@ describe(`Blog MarkdownRepository`, () => {
describe(`Finding by Slug`, () => {
it(`should return null if there's neither a blog post nor a book review with the slug`, async () => {
// WHEN
const markdownFile = repository.getBlogPostBySlug("non-existent-slug");
const markdownFile = repository.getBlogPostBySlug('non-existent-slug');
// THEN
expect(markdownFile).toBeNull();
@ -67,29 +60,27 @@ describe(`Blog MarkdownRepository`, () => {
describe(`Deleting markdown files`, () => {
let repository: MarkdownRepository;
const currentDirectory = dirname(import.meta.url.replace("file://", ""));
const currentDirectory = dirname(import.meta.url.replace('file://', ''));
beforeAll(async () => {
repository = await MarkdownRepository.fromViteGlobImport(
blogPostImport,
bookReviewImport,
snoutStreetPostImport,
snoutStreetPostImport
);
const resolvedPath = resolve(
`${currentDirectory}/test-fixtures/test-file.md`,
);
const resolvedPath = resolve(`${currentDirectory}/test-fixtures/test-file.md`);
await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml);
});
it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => {
// GIVEN
const theFileName = "non-existent-file.md";
const theFileName = 'non-existent-file.md';
// WHEN/THEN
expect(async () =>
repository.deleteBlogPostMarkdownFile(theFileName),
).rejects.toThrowError(`File 'non-existent-file.md' not found.`);
expect(async () => repository.deleteBlogPostMarkdownFile(theFileName)).rejects.toThrowError(
`File 'non-existent-file.md' not found.`
);
});
it(`should successfully delete a file when it does exist`, async () => {

View file

@ -1,20 +1,17 @@
import { writeFile, unlink, existsSync } from "fs";
import { writeFile, unlink, existsSync } from 'fs';
import { BlogPost } from "./BlogPost.js";
import { MarkdownFile } from "./MarkdownFile.js";
import { BlogPostSet } from "./BlogPostSet.js";
import { BookReviewSet } from "./BookReviewSet.js";
import { BookReview } from "./BookReview.js";
import { BlogPost } from './BlogPost.js';
import { MarkdownFile } from './MarkdownFile.js';
import { BlogPostSet } from './BlogPostSet.js';
import { BookReviewSet } from './BookReviewSet.js';
import { BookReview } from './BookReview.js';
// We have to duplicate the `../..` here because import.meta must have a static string,
// and it (rightfully) cannot have dynamic locations
const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, {
as: "raw",
as: 'raw',
});
const bookReviewsMetaGlobImport = import.meta.glob(
`../../content/book-reviews/*.md`,
{ as: "raw" },
);
const bookReviewsMetaGlobImport = import.meta.glob(`../../content/book-reviews/*.md`, { as: 'raw' });
interface BlogPostFrontmatterValues {
title: string;
@ -44,16 +41,12 @@ export class MarkdownRepository {
this.bookReviews = new BookReviewSet(bookReviews);
}
public static async singleton(
forceRefresh = false,
): Promise<MarkdownRepository> {
public static async singleton(forceRefresh = false): Promise<MarkdownRepository> {
if (forceRefresh || !this._singleton) {
console.log(
`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`,
);
console.log(`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`);
this._singleton = await MarkdownRepository.fromViteGlobImport(
blogPostMetaGlobImport,
bookReviewsMetaGlobImport,
bookReviewsMetaGlobImport
);
}
@ -62,7 +55,7 @@ export class MarkdownRepository {
public static async fromViteGlobImport(
blogGlobImport: any,
bookReviewGlobImport: any,
bookReviewGlobImport: any
): Promise<MarkdownRepository> {
let fileImports: MarkdownFile<BlogPostFrontmatterValues>[] = [];
let blogPosts: BlogPost[] = [];
@ -71,16 +64,9 @@ export class MarkdownRepository {
const blogPostFiles = Object.entries(blogGlobImport);
for (const blogPostFile of blogPostFiles) {
const [filename, module] = blogPostFile as [
string,
() => Promise<string>,
];
const [filename, module] = blogPostFile as [string, () => Promise<string>];
try {
const markdownFile =
await MarkdownFile.build<BlogPostFrontmatterValues>(
filename,
await module(),
);
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(filename, await module());
const blogPost = new BlogPost({
excerpt: markdownFile.excerpt,
@ -104,16 +90,9 @@ export class MarkdownRepository {
}
for (const bookReviewFile of Object.entries(bookReviewGlobImport)) {
const [filename, module] = bookReviewFile as [
string,
() => Promise<string>,
];
const [filename, module] = bookReviewFile as [string, () => Promise<string>];
try {
const markdownFile =
await MarkdownFile.build<BookReviewFrontmatterValues>(
filename,
await module(),
);
const markdownFile = await MarkdownFile.build<BookReviewFrontmatterValues>(filename, await module());
const bookReview = new BookReview({
author: markdownFile.frontmatter.author,
@ -136,33 +115,21 @@ export class MarkdownRepository {
}
}
console.log(
`[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`,
);
console.log(`[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`);
const repository = new MarkdownRepository(blogPosts, bookReviews);
console.log(`[MarkdownRepository::fromViteGlobImport] Built all posts.`);
return repository;
}
getBlogPostBySlug(slug: string): BlogPost | null {
return (
this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ??
null
);
return this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ?? null;
}
getBookReviewBySlug(slug: string): BookReview | null {
return (
this.bookReviews.bookReviews.find(
(bookReview) => bookReview.slug === slug,
) ?? null
);
return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null;
}
async createBlogPostMarkdownFile(
resolvedPath: string,
contents: string,
): Promise<BlogPost> {
async createBlogPostMarkdownFile(resolvedPath: string, contents: string): Promise<BlogPost> {
return new Promise<void>((resolve, reject) => {
writeFile(resolvedPath, contents, (err) => {
if (err) {
@ -177,10 +144,7 @@ export class MarkdownRepository {
resolve();
});
}).then(async () => {
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(
resolvedPath,
contents,
);
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(resolvedPath, contents);
const blogPost = new BlogPost({
html: markdownFile.html,

View file

@ -1,6 +1,6 @@
import { SimplePasswordAuthenticator } from './simple-password-authenticator';
import { it, expect} from 'vitest'
import { it, expect } from 'vitest';
it('should do nothing when things are valid', () => {
// GIVEN
@ -11,10 +11,10 @@ it('should do nothing when things are valid', () => {
//
expect(result).toBeTruthy();
})
});
it('should not authenticate when the password is invalid', () => {
// GIVEN
// GIVEN
const authenticator = new SimplePasswordAuthenticator('expected-password');
// WHEN
@ -22,5 +22,4 @@ it('should not authenticate when the password is invalid', () => {
// THEN
expect(result).toBeFalsy();
})
});

View file

@ -1,6 +1,6 @@
import type { Authenticator } from './Authenticator';
export class SimplePasswordAuthenticator implements Authenticator{
export class SimplePasswordAuthenticator implements Authenticator {
constructor(private readonly password: string) {
if (this.password === undefined) {
throw new Error('Password must be defined');

View file

@ -1,14 +1,14 @@
export interface SunriseOrSunsetPhotoSet {
total: number
total_pages: number
search_term: string
results: SunriseOrSunsetPhoto[]
total: number;
total_pages: number;
search_term: string;
results: SunriseOrSunsetPhoto[];
}
export interface SunriseOrSunsetPhoto {
id: string
description: string
username: string
username_url: string
small_url: string
id: string;
description: string;
username: string;
username_url: string;
small_url: string;
}

View file

@ -1,13 +1,13 @@
import { notStrictEqual } from "node:assert";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "../../generated/prisma/client.js";
import { env } from "$env/dynamic/private";
import { notStrictEqual } from 'node:assert';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
import { PrismaClient } from '../../generated/prisma/client.js';
import { env } from '$env/dynamic/private';
export class PrismaClientFactory {
private constructor(private readonly databaseUrl: string) {}
public static fromEnv(): PrismaClientFactory {
const value = env.PRIVATE_DATABASE_URL ?? "";
notStrictEqual(value, "", `"env.PRIVATE_DATABASE_URL" must be defined`);
const value = env.PRIVATE_DATABASE_URL ?? '';
notStrictEqual(value, '', `"env.PRIVATE_DATABASE_URL" must be defined`);
return new PrismaClientFactory(value);
}

View file

@ -1,16 +1,16 @@
import { redirect } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types.js";
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types.js';
import { CookieAuthentication } from '$lib/blog/auth/CookieAuthentication.js';
export const load: LayoutServerLoad = ({ cookies, route }) => {
const auth = new CookieAuthentication(cookies)
const isAuthd = auth.isAuthdAsAdmin
const auth = new CookieAuthentication(cookies);
const isAuthd = auth.isAuthdAsAdmin;
if (route.id === '/admin/login' && isAuthd) {
return redirect(307, '/admin')
return redirect(307, '/admin');
} else if (!isAuthd && route.id !== '/admin/login') {
return redirect(307, '/admin/login')
return redirect(307, '/admin/login');
}
return {}
}
return {};
};

View file

@ -1,25 +1,24 @@
import { PRIVATE_ADMIN_AUTH_TOKEN } from "$env/static/private";
import { redirect } from "@sveltejs/kit";
import type { Actions} from "./$types.js";
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
import { PRIVATE_ADMIN_AUTH_TOKEN } from '$env/static/private';
import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types.js';
import { CookieAuthentication } from '$lib/blog/auth/CookieAuthentication.js';
export const actions = {
default: async ({cookies, request}) => {
const formData = await request.formData()
const token = formData.get('token')
default: async ({ cookies, request }) => {
const formData = await request.formData();
const token = formData.get('token');
const isAuthd = PRIVATE_ADMIN_AUTH_TOKEN === token;
const auth = new CookieAuthentication(cookies)
const auth = new CookieAuthentication(cookies);
auth.setAdminAuthentication(isAuthd)
auth.setAdminAuthentication(isAuthd);
if (isAuthd) {
return redirect(307, '/admin')
return redirect(307, '/admin');
}
return {
isAuthd
}
}
} satisfies Actions
isAuthd,
};
},
} satisfies Actions;

View file

@ -1,7 +1,7 @@
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
import { redirect, type ServerLoad } from "@sveltejs/kit";
import { CookieAuthentication } from '$lib/blog/auth/CookieAuthentication.js';
import { redirect, type ServerLoad } from '@sveltejs/kit';
export const GET: ServerLoad = ({ cookies }) => {
new CookieAuthentication(cookies).logout();
redirect(307, "/");
redirect(307, '/');
};

View file

@ -1,15 +1,12 @@
import { writeFile } from "node:fs/promises";
import { Buffer } from "node:buffer";
import { join } from "node:path";
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
import type { Actions, ServerLoad } from "@sveltejs/kit";
import { randomUUID } from "crypto";
import { PRIVATE_PHOTO_UPLOAD_DIR } from '$env/static/private';
import type { Actions, ServerLoad } from '@sveltejs/kit';
import { LocalFileRepository } from '$lib/LocalFileRepository.js';
export const load: ServerLoad = async ({ locals }) => {
const photos = await locals.prisma.photoPost.findMany({
select: {
id: true,
filePath: true,
fileName: true,
title: true,
description: true,
@ -22,20 +19,17 @@ export const load: ServerLoad = async ({ locals }) => {
export const actions = {
default: async ({ request, locals }) => {
const formData = await request.formData();
const file = formData.get("file") as File;
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const file = formData.get('file') as File;
const title = formData.get('title') as string;
const description = formData.get('description') as string;
const filetype = file.type.split("/")[1];
const fileName = `${randomUUID()}.${filetype}`;
const fileLocation = join(PRIVATE_PHOTO_UPLOAD_DIR, fileName);
const fileContentBuffer = await file.arrayBuffer();
await writeFile(fileLocation, Buffer.from(fileContentBuffer));
const fileRepo = new LocalFileRepository(PRIVATE_PHOTO_UPLOAD_DIR);
const { fileName, filePath } = await fileRepo.saveFile(file);
await locals.prisma.photoPost.create({
data: {
fileName,
filePath,
title,
description,
createdAt: new Date(),

View file

@ -0,0 +1,51 @@
<script lang="ts">
import { page } from "$app/state";
import type { PhotoPost } from "../../../../generated/prisma/client.js";
interface Props {
photo: PhotoPost
}
const { photo }: Props = $props()
</script>
<li class="item">
<a href={`/admin/photos/${photo.id}`} class="no-icon">
<img width="250" src={`/image/${photo.fileName}`} alt={photo.title} />
</a>
<input type="text" name="id" value={`${page.url.protocol}//${page.url.host}/image/${photo.fileName}`} />
<div class="text">
<a href={`/admin/photos/${photo.id}`}>{photo.title}</a>
{#if photo.description}
<p>{photo.description}</p>
{/if}
</div>
</li>
<style>
.item {
--photo-width: 250px;
display: flex;
flex-direction: column;
justify-content: center;
img {
width: var(--photo--width);
}
.text {
width: var(--photo-width);
}
}
.item img {
width: 250px;
height: auto;
height: fit-content;
}
</style>

View file

@ -1,15 +1,13 @@
<script lang="ts">
import type { PhotoPost } from "../../../../generated/prisma/client.js";
import FeedItem from "./FeedItem.svelte";
const { photos }: { photos: PhotoPost[] } = $props();
</script>
<ul class="photo-feed">
{#each photos as photo, id}
<li class="item">
<img width="250" src={`/image/${photo.fileName}`} alt={photo.title} />
<p>{photo.title}</p>
</li>
<FeedItem {photo} />
{/each}
</ul>
@ -21,15 +19,4 @@
list-style: none;
}
.item {
display: flex;
flex-direction: column;
justify-content: center;
}
.item img {
width: 250px;
height: auto;
height: fit-content;
}
</style>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import { page } from "$app/state";
</script>
<section>
<h1>Photo error!</h1>
<p>{page.error.message}</p>
<a href="/admin/photos">Back to photos</a>
</section>

View file

@ -0,0 +1,77 @@
import { error, redirect, type Actions } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types.js";
import { LocalFileRepository } from "$lib/LocalFileRepository.js";
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
import type { Prisma } from "../../../../../generated/prisma/client.js";
export const load: PageServerLoad = async ({ params, locals }) => {
const { id } = params;
const photo = await locals.prisma.photoPost.findFirst({
where: {
id: Number(id),
},
});
if (!photo) {
return error(404, { message: `Photo with ID ${id} not found` });
}
return { photo };
};
export const actions = {
update: async ({ request, locals, params }) => {
const formData = await request.formData();
const { id } = params;
const fileRepo = new LocalFileRepository(PRIVATE_PHOTO_UPLOAD_DIR);
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const file = formData.get("file") as File | null;
const options: Prisma.PhotoPostUpdateInput = {
title,
description,
};
if (file && file.size > 0) {
const saveResult = await fileRepo.saveFile(file);
options.fileName = saveResult.fileName;
options.filePath = saveResult.filePath;
}
await locals.prisma.photoPost.update({
where: { id: Number(id) },
data: options,
});
return { success: true };
},
delete: async ({ locals, params }) => {
const { id } = params;
const photoId = Number(id);
const fileRepo = new LocalFileRepository(PRIVATE_PHOTO_UPLOAD_DIR);
const photo = await locals.prisma.photoPost.findUnique({
where: { id: photoId },
select: {
id: true,
filePath: true,
},
});
if (!photo) {
return error(404, { message: `Photo with ID ${id} not found` });
}
await locals.prisma.photoPost.delete({
where: { id: photoId },
});
await fileRepo.deleteFile(photo.filePath);
throw redirect(303, "/admin/photos");
},
} satisfies Actions;

View file

@ -0,0 +1,19 @@
<script lang="ts">
import type { PageProps } from "./$types.js";
import EditPhotoForm from "./EditPhotoForm.svelte";
const { data, form }: PageProps = $props()
const success = form?.success ?? false;
</script>
<div>
<a class="breadcrumb" href="/admin/photos">Photos</a>
<h1>{data.photo.title}</h1>
<EditPhotoForm photo={data.photo} />
{#if success}
<p>Photo updated successfully!</p>
{/if}
</div>

View file

@ -0,0 +1,131 @@
<script lang="ts">
import type { PhotoPost } from "../../../../../generated/prisma/client.js";
interface Props {
photo: PhotoPost;
}
const { photo }: Props = $props();
let imgElement: HTMLImageElement;
let deleteDialogElement: HTMLDialogElement;
let title = $state(photo.title);
let description = $state(photo.description);
let isDeleteDialogOpen = $state(false);
const handleImageChange = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result) {
imgElement.src = e.target.result as string;
}
};
reader.readAsDataURL(file);
}
};
const openDeleteDialog = () => {
isDeleteDialogOpen = true;
deleteDialogElement.showModal();
};
const closeDeleteDialog = () => {
isDeleteDialogOpen = false;
if (deleteDialogElement.open) {
deleteDialogElement.close();
}
};
const handleDeleteDialogClose = () => {
isDeleteDialogOpen = false;
};
</script>
<form
class="cms-form"
method="POST"
enctype="multipart/form-data"
action="?/update"
>
<div class="image-container">
<img
src={`/image/${photo.fileName}`}
alt={photo.title}
class="preview"
bind:this={imgElement}
/>
</div>
<div class="field">
<label for="title">Title</label>
<input type="text" id="title" name="title" bind:value={title} />
</div>
<div class="field">
<label for="description">Description</label>
<textarea
id="description"
name="description"
rows="4"
bind:value={description}
></textarea>
</div>
<div class="field">
<label for="file">File</label>
<input type="file" id="file" name="file" onchange={handleImageChange} />
</div>
<div class="actions">
<input type="submit" value="Update Photo" class="thomaswilson-button" />
<button
type="button"
class="thomaswilson-button danger"
onclick={openDeleteDialog}
>
Delete photo
</button>
</div>
</form>
<dialog
class="confirm-dialog"
bind:this={deleteDialogElement}
onclose={handleDeleteDialogClose}
>
<p>Delete this photo permanently?</p>
<div class="dialog-actions">
<button
type="button"
class="thomaswilson-button"
onclick={closeDeleteDialog}
>
No
</button>
<form method="POST" action="?/delete">
<button type="submit" class="thomaswilson-button delete-button"
>Yes, delete</button
>
</form>
</div>
</dialog>
<style>
.image-container {
display: flex;
justify-content: center;
}
.actions {
display: flex;
gap: 0.5rem;
}
img.preview {
height: 250px;
}
</style>

View file

@ -0,0 +1,7 @@
import type { PageServerLoad } from '../upload/[id]/$types.js';
export const load: PageServerLoad = async ({ params }) => {
const { id } = params;
return { id };
};

View file

@ -1,15 +1,15 @@
import { SunriseSunsetController } from './SunriseSunsetController.js'
import { SunriseSunsetController } from './SunriseSunsetController.js';
const controller = new SunriseSunsetController()
const controller = new SunriseSunsetController();
export const GET = async () => {
const now = new Date()
const body = controller.getSunriseSunsetPhotoForDate(now)
const now = new Date();
const body = controller.getSunriseSunsetPhotoForDate(now);
const response = {
status: 200,
body,
}
};
return new Response(JSON.stringify(response.body))
}
return new Response(JSON.stringify(response.body));
};

View file

@ -1,22 +1,19 @@
import { it, describe, expect, beforeAll } from 'vitest'
import {
type ISunriseSunsetController,
SunriseSunsetController,
} from './SunriseSunsetController'
import { it, describe, expect, beforeAll } from 'vitest';
import { type ISunriseSunsetController, SunriseSunsetController } from './SunriseSunsetController';
describe('SunriseSunsetController', () => {
let controller: ISunriseSunsetController
let controller: ISunriseSunsetController;
beforeAll(() => {
controller = new SunriseSunsetController()
})
controller = new SunriseSunsetController();
});
it(`Should return a known photo for a known date`, () => {
// GIVEN
const aKnownDate = new Date('2023-01-24T14:00Z')
const aKnownDate = new Date('2023-01-24T14:00Z');
// WHEN
const photo = controller.getSunriseSunsetPhotoForDate(aKnownDate)
const photo = controller.getSunriseSunsetPhotoForDate(aKnownDate);
// THEN
expect(photo).toStrictEqual({
@ -30,17 +27,17 @@ describe('SunriseSunsetController', () => {
'https://images.unsplash.com/photo-1475656106224-d72c2ab53e8d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=Mnw0MDEyNTV8MHwxfHNlYXJjaHw5M3x8c3VucmlzZXxlbnwwfHx8fDE2NzQ1MDI4MzQ&ixlib=rb-4.0.3&q=80&w=400',
sunrise_or_sunset: 'sunrise',
},
})
})
});
});
it(`should return null when there is no photo for the day`, () => {
// GIVEN
const aDateWithoutPhoto = new Date('2020-01-01T00:00Z')
const aDateWithoutPhoto = new Date('2020-01-01T00:00Z');
// WHEN
const photo = controller.getSunriseSunsetPhotoForDate(aDateWithoutPhoto)
const photo = controller.getSunriseSunsetPhotoForDate(aDateWithoutPhoto);
// THEN
expect(photo).toBeNull()
})
})
expect(photo).toBeNull();
});
});

View file

@ -1,28 +1,28 @@
import data from './data.json'
import { format as formatDate } from 'date-fns'
import data from './data.json';
import { format as formatDate } from 'date-fns';
type Daytime = 'sunrise' | 'sunset'
type Daytime = 'sunrise' | 'sunset';
interface DailyPhoto {
date: string // e.g. "2023-01-24"
date: string; // e.g. "2023-01-24"
photo: {
id: string
description: string
username: string
username_url: string
small_url: string
sunrise_or_sunset: Daytime
}
id: string;
description: string;
username: string;
username_url: string;
small_url: string;
sunrise_or_sunset: Daytime;
};
}
export interface ISunriseSunsetController {
getSunriseSunsetPhotoForDate(date: Date): DailyPhoto | null
getSunriseSunsetPhotoForDate(date: Date): DailyPhoto | null;
}
export class SunriseSunsetController implements ISunriseSunsetController {
private data: DailyPhoto[] = data.photos as any
private data: DailyPhoto[] = data.photos as any;
getSunriseSunsetPhotoForDate(date) {
const formattedDate = formatDate(date, 'yyyy-MM-dd')
const formattedDate = formatDate(date, 'yyyy-MM-dd');
return this.data.find((photo) => photo.date === formattedDate) ?? null
return this.data.find((photo) => photo.date === formattedDate) ?? null;
}
}

View file

@ -4,16 +4,16 @@ import wainwrights from '../../../content/wainwrights/wainwrights.json';
export const GET = async ({ url }) => {
try {
return json({
wainwrights
wainwrights,
});
} catch (error) {
console.error({ error: JSON.stringify(error) });
return json(
{
error: 'Could not fetch wainwrights' + error
error: 'Could not fetch wainwrights' + error,
},
{
status: 500
status: 500,
}
);
}

View file

@ -3,10 +3,10 @@ import {
type BlogItem,
type BlogPostListItem,
type BookReviewListItem,
} from "$lib/blog/BlogController.js";
import type { BookReview } from "$lib/blog/BookReview.js";
import type { Load } from "@sveltejs/kit";
import { differenceInCalendarDays, getYear } from "date-fns";
} from '$lib/blog/BlogController.js';
import type { BookReview } from '$lib/blog/BookReview.js';
import type { Load } from '@sveltejs/kit';
import { differenceInCalendarDays, getYear } from 'date-fns';
export const prerender = true;
@ -24,13 +24,13 @@ export const load: Load = async ({}) => {
const numberOfPosts = posts.length;
const firstPost = posts[numberOfPosts - 1];
const numberOfBlogPostsThisYear: number = posts.filter(
(post) => getYear(new Date(post.date)) === currentYear,
(post) => getYear(new Date(post.date)) === currentYear
).length;
const postsGroupedByMonth = posts.reduce((grouped, post) => {
const yearDate = Intl.DateTimeFormat("en-gb", {
year: "numeric",
month: "long",
const yearDate = Intl.DateTimeFormat('en-gb', {
year: 'numeric',
month: 'long',
}).format(new Date(post.date));
const index = grouped.findIndex((entry) => entry.yearDate === yearDate);

View file

@ -1,8 +1,8 @@
import { access, readFile } from "node:fs/promises";
import { constants } from "node:fs";
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
import * as path from "node:path";
import { error, type ServerLoad } from "@sveltejs/kit";
import { access, readFile } from 'node:fs/promises';
import { constants } from 'node:fs';
import { PRIVATE_PHOTO_UPLOAD_DIR } from '$env/static/private';
import * as path from 'node:path';
import { error, type ServerLoad } from '@sveltejs/kit';
export const GET: ServerLoad = async ({ params, locals }) => {
const { filename } = params;
@ -14,12 +14,13 @@ export const GET: ServerLoad = async ({ params, locals }) => {
.catch(() => false);
if (!fileExists) {
return error(404, "File not found");
console.warn(`File with name ${filename} not found.`);
return error(404, 'File not found');
}
const file = await readFile(proposedFilePath);
const fileExt = path.extname(filename);
const contentType = `image/${fileExt.replace(".", "")}`;
const contentType = `image/${fileExt.replace('.', '')}`;
return new Response(file, { headers: { "Content-Type": contentType } });
return new Response(file, { headers: { 'Content-Type': contentType } });
};

View file

@ -1,7 +1,5 @@
import type { PageServerLoad } from "./$types.js";
import type { PageServerLoad } from './$types.js';
export const load: PageServerLoad = async({}) => {
return {
}
}
export const load: PageServerLoad = async ({}) => {
return {};
};

View file

@ -1,7 +1,5 @@
import type { PageServerLoad } from "./$types.js";
import type { PageServerLoad } from './$types.js';
export const load: PageServerLoad = async({}) => {
return {
}
}
export const load: PageServerLoad = async ({}) => {
return {};
};

View file

@ -10,6 +10,6 @@ export async function load({ fetch }: LoadEvent): Promise<{ wainwrights: Wainwri
});
return {
wainwrights
wainwrights,
};
}

View file

@ -1,3 +1,105 @@
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism-solarizedlight&languages=markup+css+clike+javascript */
code[class*=language-],pre[class*=language-]{color:#657b83;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{background:#073642}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{background:#073642}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background-color:#fdf6e3}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#93a1a1}.token.punctuation{color:#586e75}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#268bd2}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string,.token.url{color:#2aa198}.token.entity{color:#657b83;background:#eee8d5}.token.atrule,.token.attr-value,.token.keyword{color:#859900}.token.class-name,.token.function{color:#b58900}.token.important,.token.regex,.token.variable{color:#cb4b16}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
code[class*='language-'],
pre[class*='language-'] {
color: #657b83;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
pre[class*='language-']::-moz-selection {
background: #073642;
}
code[class*='language-'] ::selection,
code[class*='language-']::selection,
pre[class*='language-'] ::selection,
pre[class*='language-']::selection {
background: #073642;
}
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background-color: #fdf6e3;
}
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
}
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog {
color: #93a1a1;
}
.token.punctuation {
color: #586e75;
}
.token.namespace {
opacity: 0.7;
}
.token.boolean,
.token.constant,
.token.deleted,
.token.number,
.token.property,
.token.symbol,
.token.tag {
color: #268bd2;
}
.token.attr-name,
.token.builtin,
.token.char,
.token.inserted,
.token.selector,
.token.string,
.token.url {
color: #2aa198;
}
.token.entity {
color: #657b83;
background: #eee8d5;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #859900;
}
.token.class-name,
.token.function {
color: #b58900;
}
.token.important,
.token.regex,
.token.variable {
color: #cb4b16;
}
.token.bold,
.token.important {
font-weight: 700;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View file

@ -1,6 +1,6 @@
@font-face {
font-family: "FivoSansModern-Regular";
src: url("/FivoSansModern-Regular.otf");
font-family: 'FivoSansModern-Regular';
src: url('/FivoSansModern-Regular.otf');
font-display: swap;
}
@ -21,19 +21,24 @@
--gray-900: #212529;
--gray-950: #1a1e23;
--gray-1000: #0a0c0e;
--colour-danger: red;
--font-family-mono: monospace;
--font-family-title: "FivoSansModern-Regular", sans-serif;
--font-family-title: 'FivoSansModern-Regular', sans-serif;
--font-family-sans:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-family-serif: Georgia, Cambria, "Times New Roman", Times, serif;
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-family-serif: Georgia, Cambria, 'Times New Roman', Times, serif;
--line-height: 120%;
--line-height-sm: 120%;
--line-height-md: 140%;
--line-height-lg: 145%;
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--font-size-xs: 11px;
--font-size-sm: 13px;
--font-size-base: 1rem;
@ -85,9 +90,7 @@ body {
flex-direction: column;
align-items: center;
justify-content: center;
min-height: calc(
100vh - var(--navbar-height) - calc(2 * var(--container-padding))
);
min-height: calc(100vh - var(--navbar-height) - calc(2 * var(--container-padding)));
padding: var(--container-padding);
}
@ -177,6 +180,11 @@ ol {
text-decoration: var(--btn-text-decoration);
}
.thomaswilson-button.danger {
color: var(--colour-danger);
border: 1px solid var(--colour-danger);
}
.thomaswilson-button:hover {
text-decoration: none;
cursor: pointer;
@ -200,7 +208,7 @@ img {
}
a::after {
content: url("/assets/icons/link.svg");
content: url('/assets/icons/link.svg');
margin-left: 3px;
}
@ -210,7 +218,17 @@ sup a::after {
}
a.no-icon::after {
content: "";
content: '';
display: none;
}
a.breadcrumb {
color: var(--gray-600);
font-size: var(--font-size-sm);
}
a.breadcrumb::after {
content: '';
display: none;
}
@ -260,3 +278,96 @@ a.no-icon::after {
color: var(--colour-scheme-text);
background-color: var(--colour-scheme-bg);
}
dialog.confirm-dialog {
border: 1px solid var(--colour-scheme-border);
border-radius: 4px;
padding: 16px;
background-color: var(--colour-scheme-background);
background-color: var(--colour-scheme-background);
color: var(--colour-scheme-text);
}
.confirm-dialog .delete-button {
border: 1px solid var(--colour-danger);
background-color: var(--colour-scheme-background);
color: var(--colour-danger);
}
.delete-button:hover {
background-color: #ffe3e3;
}
.delete-dialog::backdrop {
background: rgb(0 0 0 / 0.3);
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.75rem;
}
/**
* SECTION: CMS
*/
.cms-form {
--form-max-width: 760px;
--form-border: var(--colour-scheme-border, var(--gray-300));
--button-bg: var(--gray-900);
--button-text: var(--white);
width: min(100%, var(--form-max-width));
padding: var(--spacing-base);
border: 1px solid var(--form-border);
border-radius: var(--border-radius-md);
display: flex;
flex-direction: column;
gap: var(--spacing-base);
}>
.cms-form .field {
gap: 0.4rem;
}
.cms-form :is(input[type="text"], textarea, input[type="file"]) {
width: 100%;
box-sizing: border-box;
padding: var(--spacing-md) var(--spacing-base);
border: 1px solid var(--form-border);
border-radius: var(--border-radius-sm);
background: var(--colour-scheme-bg);
color: var(--colour-scheme-text);
}
.cms-form textarea {
min-height: 7rem;
resize: vertical;
}
.actions {
display: flex;
justify-content: flex-end;
}
.submit-button:hover {
opacity: 0.92;
}
@media (max-width: 640px) {
.cms-form {
padding: 0.85rem;
border-radius: 8px;
}
.actions {
justify-content: stretch;
}
.submit-button {
width: 100%;
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,9 @@
import adapter from "@sveltejs/adapter-node";
import preprocess from "svelte-preprocess";
import adapter from '@sveltejs/adapter-node';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: [".svelte", ".md"],
extensions: ['.svelte', '.md'],
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [preprocess()],
@ -11,13 +11,13 @@ const config = {
kit: {
adapter: adapter({ split: false }),
alias: {
$lib: "/src/lib",
$srcPrisma: "/src/prisma",
$generatedPrisma: "/generated/prisma/*",
$lib: '/src/lib',
$srcPrisma: '/src/prisma',
$generatedPrisma: '/generated/prisma/*',
},
env: {
publicPrefix: "PUBLIC_",
privatePrefix: "PRIVATE_",
publicPrefix: 'PUBLIC_',
privatePrefix: 'PRIVATE_',
},
},
};

View file

@ -1,14 +1,8 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()],
resolve: {
alias: {
$lib: "/src/lib",
$srcPrisma: "/src/prisma",
},
},
};
export default config;

View file

@ -4,6 +4,7 @@ export default {
alias: {
$lib: '/src/lib',
$srcPrisma: '/src/prisma',
$generatedPrisma: '/generated/prisma',
},
},
test: {