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'], ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: { settings: {
'svelte3/typescript': () => require('typescript') 'svelte3/typescript': () => require('typescript'),
}, },
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020 ecmaVersion: 2020,
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: true node: true,
} },
}; };

View file

@ -36,6 +36,50 @@ export type DateTimeFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string 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> = { export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] in?: number[]
@ -66,6 +110,54 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel> _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> = { export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] in?: number[]
@ -88,6 +180,45 @@ export type NestedDateTimeFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string 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> = { export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] in?: number[]
@ -129,4 +260,63 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel> _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", "clientVersion": "7.4.2",
"engineVersion": "94a226be1cf2967af2541cca5529f0f7ba866919", "engineVersion": "94a226be1cf2967af2541cca5529f0f7ba866919",
"activeProvider": "sqlite", "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": { "runtimeDataModel": {
"models": {}, "models": {},
"enums": {}, "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 = { 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\"]"), 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: "KwsQBRwAACIAMB0AAAQAEB4AACIAMB8CAAAAASBAACQAIQEAAAABACABAAAAAQAgBRwAACIAMB0AAAQAEB4AACIAMB8CACMAISBAACQAIQADAAAABAAgAwAABQAwBAAAAQAgAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACACHwIAAAABIEAAAAABAQgAAAkAIAIfAgAAAAEgQAAAAAEBCAAACwAwAQgAAAsAMAIfAgArACEgQAAqACECAAAAAQAgCAAADgAgAh8CACsAISBAACoAIQIAAAAEACAIAAAQACACAAAABAAgCAAAEAAgAwAAAAEAIA8AAAkAIBAAAA4AIAEAAAABACABAAAABAAgBRUAACUAIBYAACYAIBcAACkAIBgAACgAIBkAACcAIAUcAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEDAAAABAAgAwAAFgAwFAAAFwAgAwAAAAQAIAMAAAUAMAQAAAEAIAUcAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACENFQAAHgAgFgAAIQAgFwAAHgAgGAAAHgAgGQAAHgAgIQIAAAABIgIAAAAEIwIAAAAEJAIAAAABJQIAAAABJgIAAAABJwIAAAABKAIAIAAhCxUAAB4AIBgAAB8AIBkAAB8AICFAAAAAASJAAAAABCNAAAAABCRAAAAAASVAAAAAASZAAAAAASdAAAAAAShAAB0AIQsVAAAeACAYAAAfACAZAAAfACAhQAAAAAEiQAAAAAQjQAAAAAQkQAAAAAElQAAAAAEmQAAAAAEnQAAAAAEoQAAdACEIIQIAAAABIgIAAAAEIwIAAAAEJAIAAAABJQIAAAABJgIAAAABJwIAAAABKAIAHgAhCCFAAAAAASJAAAAABCNAAAAABCRAAAAAASVAAAAAASZAAAAAASdAAAAAAShAAB8AIQ0VAAAeACAWAAAhACAXAAAeACAYAAAeACAZAAAeACAhAgAAAAEiAgAAAAQjAgAAAAQkAgAAAAElAgAAAAEmAgAAAAEnAgAAAAEoAgAgACEIIQgAAAABIggAAAAEIwgAAAAEJAgAAAABJQgAAAABJggAAAABJwgAAAABKAgAIQAhBRwAACIAMB0AAAQAEB4AACIAMB8CACMAISBAACQAIQghAgAAAAEiAgAAAAQjAgAAAAQkAgAAAAElAgAAAAEmAgAAAAEnAgAAAAEoAgAeACEIIUAAAAABIkAAAAAEI0AAAAAEJEAAAAABJUAAAAABJkAAAAABJ0AAAAABKEAAHwAhAAAAAAABKUAAAAABBSkCAAAAASoCAAAAASsCAAAAASwCAAAAAS0CAAAAAQAAAAAFFQAGFgAHFwAIGAAJGQAKAAAAAAAFFQAGFgAHFwAIGAAJGQAKAQIBAgMBBQYBBgcBBwgBCQoBCgwCCw0DDA8BDRECDhIEERMBEhQBExUCGhgFGxkL" graph: "PAsQCxwAACwAMB0AAAQAEB4AACwAMB8CAAAAASBAAC4AISFAAC8AISJAAC8AISMBADAAISQBADAAISUBADAAISYBADEAIQEAAAABACABAAAAAQAgCxwAACwAMB0AAAQAEB4AACwAMB8CAC0AISBAAC4AISFAAC8AISJAAC8AISMBADAAISQBADAAISUBADAAISYBADEAIQMhAAAyACAiAAAyACAmAAAyACADAAAABAAgAwAABQAwBAAAAQAgAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACAIHwIAAAABIEAAAAABIUAAAAABIkAAAAABIwEAAAABJAEAAAABJQEAAAABJgEAAAABAQgAAAkAIAgfAgAAAAEgQAAAAAEhQAAAAAEiQAAAAAEjAQAAAAEkAQAAAAElAQAAAAEmAQAAAAEBCAAACwAwAQgAAAsAMAgfAgA8ACEgQAA4ACEhQAA5ACEiQAA5ACEjAQA6ACEkAQA6ACElAQA6ACEmAQA7ACECAAAAAQAgCAAADgAgCB8CADwAISBAADgAISFAADkAISJAADkAISMBADoAISQBADoAISUBADoAISYBADsAIQIAAAAEACAIAAAQACACAAAABAAgCAAAEAAgAwAAAAEAIA8AAAkAIBAAAA4AIAEAAAABACABAAAABAAgCBUAADMAIBYAADQAIBcAADcAIBgAADYAIBkAADUAICEAADIAICIAADIAICYAADIAIAscAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEhQAAdACEiQAAdACEjAQAeACEkAQAeACElAQAeACEmAQAfACEDAAAABAAgAwAAFgAwFAAAFwAgAwAAAAQAIAMAAAUAMAQAAAEAIAscAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEhQAAdACEiQAAdACEjAQAeACEkAQAeACElAQAeACEmAQAfACENFQAAJAAgFgAAKwAgFwAAJAAgGAAAJAAgGQAAJAAgJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAKgAhCxUAACQAIBgAACkAIBkAACkAICdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACgAIQsVAAAhACAYAAAnACAZAAAnACAnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAmACEOFQAAJAAgGAAAJQAgGQAAJQAgJwEAAAABKAEAAAAEKQEAAAAEKgEAAAABKwEAAAABLAEAAAABLQEAAAABLgEAAAABLwEAAAABMAEAAAABMQEAIwAhDhUAACEAIBgAACIAIBkAACIAICcBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACAAIQ4VAAAhACAYAAAiACAZAAAiACAnAQAAAAEoAQAAAAUpAQAAAAUqAQAAAAErAQAAAAEsAQAAAAEtAQAAAAEuAQAAAAEvAQAAAAEwAQAAAAExAQAgACEIJwIAAAABKAIAAAAFKQIAAAAFKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAIQAhCycBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACIAIQ4VAAAkACAYAAAlACAZAAAlACAnAQAAAAEoAQAAAAQpAQAAAAQqAQAAAAErAQAAAAEsAQAAAAEtAQAAAAEuAQAAAAEvAQAAAAEwAQAAAAExAQAjACEIJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAJAAhCycBAAAAASgBAAAABCkBAAAABCoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACUAIQsVAAAhACAYAAAnACAZAAAnACAnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAmACEIJ0AAAAABKEAAAAAFKUAAAAAFKkAAAAABK0AAAAABLEAAAAABLUAAAAABMUAAJwAhCxUAACQAIBgAACkAIBkAACkAICdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACgAIQgnQAAAAAEoQAAAAAQpQAAAAAQqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAApACENFQAAJAAgFgAAKwAgFwAAJAAgGAAAJAAgGQAAJAAgJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAKgAhCCcIAAAAASgIAAAABCkIAAAABCoIAAAAASsIAAAAASwIAAAAAS0IAAAAATEIACsAIQscAAAsADAdAAAEABAeAAAsADAfAgAtACEgQAAuACEhQAAvACEiQAAvACEjAQAwACEkAQAwACElAQAwACEmAQAxACEIJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAJAAhCCdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACkAIQgnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAnACELJwEAAAABKAEAAAAEKQEAAAAEKgEAAAABKwEAAAABLAEAAAABLQEAAAABLgEAAAABLwEAAAABMAEAAAABMQEAJQAhCycBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACIAIQAAAAAAAAEyQAAAAAEBMkAAAAABATIBAAAAAQEyAQAAAAEFMgIAAAABMwIAAAABNAIAAAABNQIAAAABNgIAAAABAAAAAAUVAAYWAAcXAAgYAAkZAAoAAAAAAAUVAAYWAAcXAAgYAAkZAAoBAgECAwEFBgEGBwEHCAEJCgEKDAILDQMMDwENEQIOEgQREwESFAETFQIaGAUbGQs"
} }
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> { async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {

View file

@ -516,7 +516,13 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
export const PhotoPostScalarFieldEnum = { export const PhotoPostScalarFieldEnum = {
id: 'id', id: 'id',
createdAt: 'createdAt' createdAt: 'createdAt',
deletedAt: 'deletedAt',
publishedAt: 'publishedAt',
filePath: 'filePath',
fileName: 'fileName',
title: 'title',
description: 'description'
} as const } as const
export type PhotoPostScalarFieldEnum = (typeof PhotoPostScalarFieldEnum)[keyof typeof PhotoPostScalarFieldEnum] export type PhotoPostScalarFieldEnum = (typeof PhotoPostScalarFieldEnum)[keyof typeof PhotoPostScalarFieldEnum]
@ -530,6 +536,14 @@ export const SortOrder = {
export type SortOrder = (typeof SortOrder)[keyof typeof 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 * 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' * Reference to a field of type 'Float'
*/ */

View file

@ -69,7 +69,13 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
export const PhotoPostScalarFieldEnum = { export const PhotoPostScalarFieldEnum = {
id: 'id', id: 'id',
createdAt: 'createdAt' createdAt: 'createdAt',
deletedAt: 'deletedAt',
publishedAt: 'publishedAt',
filePath: 'filePath',
fileName: 'fileName',
title: 'title',
description: 'description'
} as const } as const
export type PhotoPostScalarFieldEnum = (typeof PhotoPostScalarFieldEnum)[keyof typeof PhotoPostScalarFieldEnum] export type PhotoPostScalarFieldEnum = (typeof PhotoPostScalarFieldEnum)[keyof typeof PhotoPostScalarFieldEnum]
@ -82,3 +88,11 @@ export const SortOrder = {
export type SortOrder = (typeof SortOrder)[keyof typeof 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 = { export type PhotoPostMinAggregateOutputType = {
id: number | null id: number | null
createdAt: Date | 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 = { export type PhotoPostMaxAggregateOutputType = {
id: number | null id: number | null
createdAt: Date | 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 = { export type PhotoPostCountAggregateOutputType = {
id: number id: number
createdAt: number createdAt: number
deletedAt: number
publishedAt: number
filePath: number
fileName: number
title: number
description: number
_all: number _all: number
} }
@ -62,16 +80,34 @@ export type PhotoPostSumAggregateInputType = {
export type PhotoPostMinAggregateInputType = { export type PhotoPostMinAggregateInputType = {
id?: true id?: true
createdAt?: true createdAt?: true
deletedAt?: true
publishedAt?: true
filePath?: true
fileName?: true
title?: true
description?: true
} }
export type PhotoPostMaxAggregateInputType = { export type PhotoPostMaxAggregateInputType = {
id?: true id?: true
createdAt?: true createdAt?: true
deletedAt?: true
publishedAt?: true
filePath?: true
fileName?: true
title?: true
description?: true
} }
export type PhotoPostCountAggregateInputType = { export type PhotoPostCountAggregateInputType = {
id?: true id?: true
createdAt?: true createdAt?: true
deletedAt?: true
publishedAt?: true
filePath?: true
fileName?: true
title?: true
description?: true
_all?: true _all?: true
} }
@ -164,6 +200,12 @@ export type PhotoPostGroupByArgs<ExtArgs extends runtime.Types.Extensions.Intern
export type PhotoPostGroupByOutputType = { export type PhotoPostGroupByOutputType = {
id: number id: number
createdAt: Date createdAt: Date
deletedAt: Date | null
publishedAt: Date | null
filePath: string
fileName: string
title: string
description: string | null
_count: PhotoPostCountAggregateOutputType | null _count: PhotoPostCountAggregateOutputType | null
_avg: PhotoPostAvgAggregateOutputType | null _avg: PhotoPostAvgAggregateOutputType | null
_sum: PhotoPostSumAggregateOutputType | null _sum: PhotoPostSumAggregateOutputType | null
@ -192,11 +234,23 @@ export type PhotoPostWhereInput = {
NOT?: Prisma.PhotoPostWhereInput | Prisma.PhotoPostWhereInput[] NOT?: Prisma.PhotoPostWhereInput | Prisma.PhotoPostWhereInput[]
id?: Prisma.IntFilter<"PhotoPost"> | number id?: Prisma.IntFilter<"PhotoPost"> | number
createdAt?: Prisma.DateTimeFilter<"PhotoPost"> | Date | string 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 = { export type PhotoPostOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
createdAt?: 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<{ export type PhotoPostWhereUniqueInput = Prisma.AtLeast<{
@ -205,11 +259,23 @@ export type PhotoPostWhereUniqueInput = Prisma.AtLeast<{
OR?: Prisma.PhotoPostWhereInput[] OR?: Prisma.PhotoPostWhereInput[]
NOT?: Prisma.PhotoPostWhereInput | Prisma.PhotoPostWhereInput[] NOT?: Prisma.PhotoPostWhereInput | Prisma.PhotoPostWhereInput[]
createdAt?: Prisma.DateTimeFilter<"PhotoPost"> | Date | string 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"> }, "id">
export type PhotoPostOrderByWithAggregationInput = { export type PhotoPostOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
createdAt?: 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 _count?: Prisma.PhotoPostCountOrderByAggregateInput
_avg?: Prisma.PhotoPostAvgOrderByAggregateInput _avg?: Prisma.PhotoPostAvgOrderByAggregateInput
_max?: Prisma.PhotoPostMaxOrderByAggregateInput _max?: Prisma.PhotoPostMaxOrderByAggregateInput
@ -223,43 +289,97 @@ export type PhotoPostScalarWhereWithAggregatesInput = {
NOT?: Prisma.PhotoPostScalarWhereWithAggregatesInput | Prisma.PhotoPostScalarWhereWithAggregatesInput[] NOT?: Prisma.PhotoPostScalarWhereWithAggregatesInput | Prisma.PhotoPostScalarWhereWithAggregatesInput[]
id?: Prisma.IntWithAggregatesFilter<"PhotoPost"> | number id?: Prisma.IntWithAggregatesFilter<"PhotoPost"> | number
createdAt?: Prisma.DateTimeWithAggregatesFilter<"PhotoPost"> | Date | string 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 = { export type PhotoPostCreateInput = {
createdAt?: Date | string createdAt?: Date | string
deletedAt?: Date | string | null
publishedAt?: Date | string | null
filePath: string
fileName: string
title: string
description?: string | null
} }
export type PhotoPostUncheckedCreateInput = { export type PhotoPostUncheckedCreateInput = {
id?: number id?: number
createdAt?: Date | string createdAt?: Date | string
deletedAt?: Date | string | null
publishedAt?: Date | string | null
filePath: string
fileName: string
title: string
description?: string | null
} }
export type PhotoPostUpdateInput = { export type PhotoPostUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string 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 = { export type PhotoPostUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string 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 = { export type PhotoPostCreateManyInput = {
id?: number id?: number
createdAt?: Date | string createdAt?: Date | string
deletedAt?: Date | string | null
publishedAt?: Date | string | null
filePath: string
fileName: string
title: string
description?: string | null
} }
export type PhotoPostUpdateManyMutationInput = { export type PhotoPostUpdateManyMutationInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string 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 = { export type PhotoPostUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string 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 = { export type PhotoPostCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
createdAt?: 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 = { export type PhotoPostAvgOrderByAggregateInput = {
@ -269,11 +389,23 @@ export type PhotoPostAvgOrderByAggregateInput = {
export type PhotoPostMaxOrderByAggregateInput = { export type PhotoPostMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
createdAt?: 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 = { export type PhotoPostMinOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
createdAt?: 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 = { export type PhotoPostSumOrderByAggregateInput = {
@ -284,6 +416,18 @@ export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string set?: Date | string
} }
export type NullableDateTimeFieldUpdateOperationsInput = {
set?: Date | string | null
}
export type StringFieldUpdateOperationsInput = {
set?: string
}
export type NullableStringFieldUpdateOperationsInput = {
set?: string | null
}
export type IntFieldUpdateOperationsInput = { export type IntFieldUpdateOperationsInput = {
set?: number set?: number
increment?: 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<{ export type PhotoPostSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
createdAt?: boolean createdAt?: boolean
deletedAt?: boolean
publishedAt?: boolean
filePath?: boolean
fileName?: boolean
title?: boolean
description?: boolean
}, ExtArgs["result"]["photoPost"]> }, ExtArgs["result"]["photoPost"]>
export type PhotoPostSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type PhotoPostSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
createdAt?: boolean createdAt?: boolean
deletedAt?: boolean
publishedAt?: boolean
filePath?: boolean
fileName?: boolean
title?: boolean
description?: boolean
}, ExtArgs["result"]["photoPost"]> }, ExtArgs["result"]["photoPost"]>
export type PhotoPostSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type PhotoPostSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
createdAt?: boolean createdAt?: boolean
deletedAt?: boolean
publishedAt?: boolean
filePath?: boolean
fileName?: boolean
title?: boolean
description?: boolean
}, ExtArgs["result"]["photoPost"]> }, ExtArgs["result"]["photoPost"]>
export type PhotoPostSelectScalar = { export type PhotoPostSelectScalar = {
id?: boolean id?: boolean
createdAt?: 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> = { export type $PhotoPostPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "PhotoPost" name: "PhotoPost"
@ -322,6 +490,12 @@ export type $PhotoPostPayload<ExtArgs extends runtime.Types.Extensions.InternalA
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: number id: number
createdAt: Date createdAt: Date
deletedAt: Date | null
publishedAt: Date | null
filePath: string
fileName: string
title: string
description: string | null
}, ExtArgs["result"]["photoPost"]> }, ExtArgs["result"]["photoPost"]>
composites: {} composites: {}
} }
@ -747,6 +921,12 @@ export interface Prisma__PhotoPostClient<T, Null = never, ExtArgs extends runtim
export interface PhotoPostFieldRefs { export interface PhotoPostFieldRefs {
readonly id: Prisma.FieldRef<"PhotoPost", 'Int'> readonly id: Prisma.FieldRef<"PhotoPost", 'Int'>
readonly createdAt: Prisma.FieldRef<"PhotoPost", 'DateTime'> 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. * 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: // This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv // npm install --save-dev prisma dotenv
import "dotenv/config"; import 'dotenv/config';
import { defineConfig } from "prisma/config"; import { defineConfig } from 'prisma/config';
export default defineConfig({ export default defineConfig({
schema: "./prisma/schema.prisma", schema: './prisma/schema.prisma',
migrations: { migrations: {
path: "./prisma/migrations", path: './prisma/migrations',
}, },
datasource: { 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 { model PhotoPost {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
deletedAt DateTime?
publishedAt DateTime?
filePath String
fileName String
title String
description String?
} }

View file

@ -44,13 +44,13 @@
method="POST" method="POST"
action="/admin/photos" action="/admin/photos"
enctype="multipart/form-data" enctype="multipart/form-data"
class="admin-form" class="cms-form"
> >
<canvas bind:this={canvasElement}></canvas> <canvas bind:this={canvasElement}></canvas>
<div class="field"> <div class="field">
<label for="title">Title</label> <label for="title">Title</label>
<input type="text" name="title" id="title" /> <input type="text" name="title" id="title" required />
</div> </div>
<div class="field"> <div class="field">
@ -59,6 +59,7 @@
type="file" type="file"
name="file" name="file"
accept="image/*" accept="image/*"
required
/> />
</div> </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 { import { describe, it, beforeEach, afterAll, beforeAll, expect, afterEach } from 'vitest';
describe, import { BlogController } from './BlogController.js';
it, import { MarkdownRepository } from './markdown-repository.js';
beforeEach, import { exampleMarkdown, exampleMarkdownFrontmatter } from './test-fixtures/example-markdown.js';
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`, () => { describe(`BlogController`, () => {
let controller: BlogController; let controller: BlogController;
@ -27,13 +16,9 @@ describe(`BlogController`, () => {
const blogPosts = await controller.getAllBlogPosts(); const blogPosts = await controller.getAllBlogPosts();
// WHEN // WHEN
const aKnownBlogPost = blogPosts.find( const aKnownBlogPost = blogPosts.find((post) => post.title === 'Vibe Check #10');
(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 aKnownBookReview = blogPosts.find((post) => post.title === "After");
const aMadeUpBlogPost = blogPosts.find(
(post) => post.title === "Some made up blog post",
);
// then // then
expect(aMadeUpBlogPost).toBeUndefined(); expect(aMadeUpBlogPost).toBeUndefined();
@ -45,9 +30,7 @@ describe(`BlogController`, () => {
describe(`getBlogPostBySlug`, () => { describe(`getBlogPostBySlug`, () => {
it(`should return null when the post doesn't exist`, async () => { it(`should return null when the post doesn't exist`, async () => {
// When // When
const shouldBeNull = await controller.getBlogPostBySlug( const shouldBeNull = await controller.getBlogPostBySlug('some-made-up-blog-post');
"some-made-up-blog-post",
);
// Then // Then
expect(shouldBeNull).toBeNull(); expect(shouldBeNull).toBeNull();
@ -55,26 +38,23 @@ describe(`BlogController`, () => {
it(`should return the blog post when it exists`, async () => { it(`should return the blog post when it exists`, async () => {
// When // When
const blogPost = await controller.getBlogPostBySlug( const blogPost = await controller.getBlogPostBySlug('2023-02-03-vibe-check-10');
"2023-02-03-vibe-check-10",
);
// Then // Then
expect(blogPost).not.toBeNull(); 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 content by slug`, () => {
describe(`Finding a blog post`, () => { describe(`Finding a blog post`, () => {
// GIVEN // GIVEN
const slugForRealBlogPost = "2023-02-03-vibe-check-10"; const slugForRealBlogPost = '2023-02-03-vibe-check-10';
const slugForFakeBlogPost = "some-made-up-blog-post"; const slugForFakeBlogPost = 'some-made-up-blog-post';
it(`should return null if there's no blog post with the slug`, async () => { it(`should return null if there's no blog post with the slug`, async () => {
// WHEN // WHEN
const blogPost = const blogPost = await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost);
await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost);
// THEN // THEN
expect(blogPost).toBeNull(); expect(blogPost).toBeNull();
@ -82,18 +62,17 @@ describe(`BlogController`, () => {
it(`should return the blog post if it exists`, async () => { it(`should return the blog post if it exists`, async () => {
// WHEN // WHEN
const blogPost = const blogPost = await controller.getAnyKindOfContentBySlug(slugForRealBlogPost);
await controller.getAnyKindOfContentBySlug(slugForRealBlogPost);
// THEN // THEN
expect(blogPost).not.toBeNull(); expect(blogPost).not.toBeNull();
expect(blogPost.title).toBe("Vibe Check #10"); expect(blogPost.title).toBe('Vibe Check #10');
}); });
}); });
describe(`Finding a book review`, () => { describe(`Finding a book review`, () => {
const realSlug = "after"; const realSlug = 'after';
const fakeSlug = "some-made-up-book-review"; const fakeSlug = 'some-made-up-book-review';
it(`should return null if there's no book review with the slug`, async () => { it(`should return null if there's no book review with the slug`, async () => {
// WHEN // WHEN
@ -109,17 +88,17 @@ describe(`BlogController`, () => {
// THEN // THEN
expect(bookReview).not.toBeNull(); expect(bookReview).not.toBeNull();
expect(bookReview.title).toBe("After"); expect(bookReview.title).toBe('After');
}); });
}); });
}); });
describe(`Creating a new blog post as a file`, () => { describe(`Creating a new blog post as a file`, () => {
const thisDirectory = import.meta.url const thisDirectory = import.meta.url
.replace("file://", "") .replace('file://', '')
.split("/") .split('/')
.filter((part) => part !== "BlogController.test.ts") .filter((part) => part !== 'BlogController.test.ts')
.join("/"); .join('/');
const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`; const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`;
let controller: BlogController; let controller: BlogController;
@ -136,10 +115,7 @@ describe(`BlogController`, () => {
const markdownContent = exampleMarkdown; const markdownContent = exampleMarkdown;
// WHEN // WHEN
const blogPost = await controller.createBlogPost( const blogPost = await controller.createBlogPost(fileName, markdownContent);
fileName,
markdownContent,
);
// THEN // THEN
expect(blogPost).not.toBeNull(); expect(blogPost).not.toBeNull();

View file

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

View file

@ -8,7 +8,10 @@ import type { BookReview } from './BookReview.js';
export class RssFeed { export class RssFeed {
private feed: Feed; 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({ this.feed = new Feed({
copyright: `All Rights Reserved Thomas Wilson 2023`, copyright: `All Rights Reserved Thomas Wilson 2023`,
id: 'https://www.thomaswilson.xyz', 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 { export class CookieAuthentication {
private readonly cookieValue: string; private readonly cookieValue: string;
private readonly cookieValueArray: string[]; private readonly cookieValueArray: string[];
public static cookieName = "auth"; public static cookieName = 'auth';
public static adminAuthRole = "admin"; public static adminAuthRole = 'admin';
constructor(private readonly cookies: Cookies) { constructor(private readonly cookies: Cookies) {
this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? ""; this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? '';
this.cookieValueArray = this.cookieValue.split(","); this.cookieValueArray = this.cookieValue.split(',');
} }
public get isAuthdAsAdmin(): boolean { public get isAuthdAsAdmin(): boolean {
@ -24,22 +24,18 @@ export class CookieAuthentication {
public logout() { public logout() {
if (!this.isAuthdAsAdmin) return; if (!this.isAuthdAsAdmin) return;
this.cookies.delete(CookieAuthentication.cookieName, { path: "/" }); this.cookies.delete(CookieAuthentication.cookieName, { path: '/' });
} }
public setAdminAuthentication(isAuthd: boolean) { public setAdminAuthentication(isAuthd: boolean) {
let value = this.cookieValue; let value = this.cookieValue;
if (isAuthd) { if (isAuthd) {
value = Array.from( value = Array.from(new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole])).join(',');
new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole]),
).join(",");
} else { } else {
value = this.cookieValueArray value = this.cookieValueArray.filter((i) => i !== CookieAuthentication.adminAuthRole).join(',');
.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 { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { MarkdownRepository } from "./markdown-repository.js"; import { MarkdownRepository } from './markdown-repository.js';
import { resolve, dirname } from "path"; 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`, { const blogPostImport = import.meta.glob(`./test-fixtures/blog-*.md`, {
as: "raw", as: 'raw',
}); });
const bookReviewImport = import.meta.glob(`./test-fixtures/book-review-*.md`, { const bookReviewImport = import.meta.glob(`./test-fixtures/book-review-*.md`, {
as: "raw", as: 'raw',
}); });
const snoutStreetPostImport = import.meta.glob( const snoutStreetPostImport = import.meta.glob(`./test-fixtures/snout-street-studio-*.md`, { as: 'raw' });
`./test-fixtures/snout-street-studio-*.md`,
{ as: "raw" },
);
const expectedHtml = `<p>This is a blog post written in markdown.</p> 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>`; <p>This is a <a href="http://www.bbc.co.uk">link</a></p>`;
@ -22,27 +19,23 @@ describe(`Blog MarkdownRepository`, () => {
let repository: MarkdownRepository; let repository: MarkdownRepository;
beforeEach(async () => { beforeEach(async () => {
repository = await MarkdownRepository.fromViteGlobImport( repository = await MarkdownRepository.fromViteGlobImport(blogPostImport, bookReviewImport);
blogPostImport,
bookReviewImport,
);
}); });
it(`should load`, async () => { it(`should load`, async () => {
// GIVEN // GIVEN
const expectedBlogPost = aBlogPost() const expectedBlogPost = aBlogPost()
.withAuthor("Thomas Wilson") .withAuthor('Thomas Wilson')
.withDate(new Date("2023-02-01T08:00:00Z")) .withDate(new Date('2023-02-01T08:00:00Z'))
.withSlug("2023-02-01-test") .withSlug('2023-02-01-test')
.withTitle("Test Blog Post") .withTitle('Test Blog Post')
.withExcerpt("This is a blog post written in markdown. This is a link") .withExcerpt('This is a blog post written in markdown. This is a link')
.withHtml(expectedHtml) .withHtml(expectedHtml)
.withFileName("blog-2023-02-01-test.md") .withFileName('blog-2023-02-01-test.md')
.build(); .build();
// WHEN // WHEN
const blogPost = const blogPost = repository.blogPosts.getBlogPostWithTitle('Test Blog Post');
repository.blogPosts.getBlogPostWithTitle("Test Blog Post");
// THEN // THEN
expect(repository).toBeDefined(); expect(repository).toBeDefined();
@ -58,7 +51,7 @@ describe(`Blog MarkdownRepository`, () => {
describe(`Finding by Slug`, () => { describe(`Finding by Slug`, () => {
it(`should return null if there's neither a blog post nor a book review with the slug`, async () => { it(`should return null if there's neither a blog post nor a book review with the slug`, async () => {
// WHEN // WHEN
const markdownFile = repository.getBlogPostBySlug("non-existent-slug"); const markdownFile = repository.getBlogPostBySlug('non-existent-slug');
// THEN // THEN
expect(markdownFile).toBeNull(); expect(markdownFile).toBeNull();
@ -67,29 +60,27 @@ describe(`Blog MarkdownRepository`, () => {
describe(`Deleting markdown files`, () => { describe(`Deleting markdown files`, () => {
let repository: MarkdownRepository; let repository: MarkdownRepository;
const currentDirectory = dirname(import.meta.url.replace("file://", "")); const currentDirectory = dirname(import.meta.url.replace('file://', ''));
beforeAll(async () => { beforeAll(async () => {
repository = await MarkdownRepository.fromViteGlobImport( repository = await MarkdownRepository.fromViteGlobImport(
blogPostImport, blogPostImport,
bookReviewImport, bookReviewImport,
snoutStreetPostImport, snoutStreetPostImport
); );
const resolvedPath = resolve( const resolvedPath = resolve(`${currentDirectory}/test-fixtures/test-file.md`);
`${currentDirectory}/test-fixtures/test-file.md`,
);
await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml); await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml);
}); });
it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => { it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => {
// GIVEN // GIVEN
const theFileName = "non-existent-file.md"; const theFileName = 'non-existent-file.md';
// WHEN/THEN // WHEN/THEN
expect(async () => expect(async () => repository.deleteBlogPostMarkdownFile(theFileName)).rejects.toThrowError(
repository.deleteBlogPostMarkdownFile(theFileName), `File 'non-existent-file.md' not found.`
).rejects.toThrowError(`File 'non-existent-file.md' not found.`); );
}); });
it(`should successfully delete a file when it does exist`, async () => { 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 { BlogPost } from './BlogPost.js';
import { MarkdownFile } from "./MarkdownFile.js"; import { MarkdownFile } from './MarkdownFile.js';
import { BlogPostSet } from "./BlogPostSet.js"; import { BlogPostSet } from './BlogPostSet.js';
import { BookReviewSet } from "./BookReviewSet.js"; import { BookReviewSet } from './BookReviewSet.js';
import { BookReview } from "./BookReview.js"; import { BookReview } from './BookReview.js';
// We have to duplicate the `../..` here because import.meta must have a static string, // We have to duplicate the `../..` here because import.meta must have a static string,
// and it (rightfully) cannot have dynamic locations // and it (rightfully) cannot have dynamic locations
const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, { const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, {
as: "raw", as: 'raw',
}); });
const bookReviewsMetaGlobImport = import.meta.glob( const bookReviewsMetaGlobImport = import.meta.glob(`../../content/book-reviews/*.md`, { as: 'raw' });
`../../content/book-reviews/*.md`,
{ as: "raw" },
);
interface BlogPostFrontmatterValues { interface BlogPostFrontmatterValues {
title: string; title: string;
@ -44,16 +41,12 @@ export class MarkdownRepository {
this.bookReviews = new BookReviewSet(bookReviews); this.bookReviews = new BookReviewSet(bookReviews);
} }
public static async singleton( public static async singleton(forceRefresh = false): Promise<MarkdownRepository> {
forceRefresh = false,
): Promise<MarkdownRepository> {
if (forceRefresh || !this._singleton) { if (forceRefresh || !this._singleton) {
console.log( console.log(`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`);
`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`,
);
this._singleton = await MarkdownRepository.fromViteGlobImport( this._singleton = await MarkdownRepository.fromViteGlobImport(
blogPostMetaGlobImport, blogPostMetaGlobImport,
bookReviewsMetaGlobImport, bookReviewsMetaGlobImport
); );
} }
@ -62,7 +55,7 @@ export class MarkdownRepository {
public static async fromViteGlobImport( public static async fromViteGlobImport(
blogGlobImport: any, blogGlobImport: any,
bookReviewGlobImport: any, bookReviewGlobImport: any
): Promise<MarkdownRepository> { ): Promise<MarkdownRepository> {
let fileImports: MarkdownFile<BlogPostFrontmatterValues>[] = []; let fileImports: MarkdownFile<BlogPostFrontmatterValues>[] = [];
let blogPosts: BlogPost[] = []; let blogPosts: BlogPost[] = [];
@ -71,16 +64,9 @@ export class MarkdownRepository {
const blogPostFiles = Object.entries(blogGlobImport); const blogPostFiles = Object.entries(blogGlobImport);
for (const blogPostFile of blogPostFiles) { for (const blogPostFile of blogPostFiles) {
const [filename, module] = blogPostFile as [ const [filename, module] = blogPostFile as [string, () => Promise<string>];
string,
() => Promise<string>,
];
try { try {
const markdownFile = const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(filename, await module());
await MarkdownFile.build<BlogPostFrontmatterValues>(
filename,
await module(),
);
const blogPost = new BlogPost({ const blogPost = new BlogPost({
excerpt: markdownFile.excerpt, excerpt: markdownFile.excerpt,
@ -104,16 +90,9 @@ export class MarkdownRepository {
} }
for (const bookReviewFile of Object.entries(bookReviewGlobImport)) { for (const bookReviewFile of Object.entries(bookReviewGlobImport)) {
const [filename, module] = bookReviewFile as [ const [filename, module] = bookReviewFile as [string, () => Promise<string>];
string,
() => Promise<string>,
];
try { try {
const markdownFile = const markdownFile = await MarkdownFile.build<BookReviewFrontmatterValues>(filename, await module());
await MarkdownFile.build<BookReviewFrontmatterValues>(
filename,
await module(),
);
const bookReview = new BookReview({ const bookReview = new BookReview({
author: markdownFile.frontmatter.author, author: markdownFile.frontmatter.author,
@ -136,33 +115,21 @@ export class MarkdownRepository {
} }
} }
console.log( console.log(`[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`);
`[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`,
);
const repository = new MarkdownRepository(blogPosts, bookReviews); const repository = new MarkdownRepository(blogPosts, bookReviews);
console.log(`[MarkdownRepository::fromViteGlobImport] Built all posts.`); console.log(`[MarkdownRepository::fromViteGlobImport] Built all posts.`);
return repository; return repository;
} }
getBlogPostBySlug(slug: string): BlogPost | null { getBlogPostBySlug(slug: string): BlogPost | null {
return ( return this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ?? null;
this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ??
null
);
} }
getBookReviewBySlug(slug: string): BookReview | null { getBookReviewBySlug(slug: string): BookReview | null {
return ( return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null;
this.bookReviews.bookReviews.find(
(bookReview) => bookReview.slug === slug,
) ?? null
);
} }
async createBlogPostMarkdownFile( async createBlogPostMarkdownFile(resolvedPath: string, contents: string): Promise<BlogPost> {
resolvedPath: string,
contents: string,
): Promise<BlogPost> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
writeFile(resolvedPath, contents, (err) => { writeFile(resolvedPath, contents, (err) => {
if (err) { if (err) {
@ -177,10 +144,7 @@ export class MarkdownRepository {
resolve(); resolve();
}); });
}).then(async () => { }).then(async () => {
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>( const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(resolvedPath, contents);
resolvedPath,
contents,
);
const blogPost = new BlogPost({ const blogPost = new BlogPost({
html: markdownFile.html, html: markdownFile.html,

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,16 @@
import { redirect } from "@sveltejs/kit"; import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from "./$types.js"; import type { LayoutServerLoad } from './$types.js';
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js"; import { CookieAuthentication } from '$lib/blog/auth/CookieAuthentication.js';
export const load: LayoutServerLoad = ({ cookies, route }) => { export const load: LayoutServerLoad = ({ cookies, route }) => {
const auth = new CookieAuthentication(cookies) const auth = new CookieAuthentication(cookies);
const isAuthd = auth.isAuthdAsAdmin const isAuthd = auth.isAuthdAsAdmin;
if (route.id === '/admin/login' && isAuthd) { if (route.id === '/admin/login' && isAuthd) {
return redirect(307, '/admin') return redirect(307, '/admin');
} else if (!isAuthd && route.id !== '/admin/login') { } 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 { PRIVATE_ADMIN_AUTH_TOKEN } from '$env/static/private';
import { redirect } from "@sveltejs/kit"; import { redirect } from '@sveltejs/kit';
import type { Actions} from "./$types.js"; import type { Actions } from './$types.js';
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js"; import { CookieAuthentication } from '$lib/blog/auth/CookieAuthentication.js';
export const actions = { export const actions = {
default: async ({cookies, request}) => { default: async ({ cookies, request }) => {
const formData = await request.formData() const formData = await request.formData();
const token = formData.get('token') const token = formData.get('token');
const isAuthd = PRIVATE_ADMIN_AUTH_TOKEN === 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) { if (isAuthd) {
return redirect(307, '/admin') return redirect(307, '/admin');
} }
return { return {
isAuthd isAuthd,
} };
} },
} satisfies Actions;
} satisfies Actions

View file

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

View file

@ -1,15 +1,12 @@
import { writeFile } from "node:fs/promises"; import { PRIVATE_PHOTO_UPLOAD_DIR } from '$env/static/private';
import { Buffer } from "node:buffer"; import type { Actions, ServerLoad } from '@sveltejs/kit';
import { join } from "node:path"; import { LocalFileRepository } from '$lib/LocalFileRepository.js';
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
import type { Actions, ServerLoad } from "@sveltejs/kit";
import { randomUUID } from "crypto";
export const load: ServerLoad = async ({ locals }) => { export const load: ServerLoad = async ({ locals }) => {
const photos = await locals.prisma.photoPost.findMany({ const photos = await locals.prisma.photoPost.findMany({
select: { select: {
id: true, id: true,
filePath: true,
fileName: true, fileName: true,
title: true, title: true,
description: true, description: true,
@ -22,20 +19,17 @@ export const load: ServerLoad = async ({ locals }) => {
export const actions = { export const actions = {
default: async ({ request, locals }) => { default: async ({ request, locals }) => {
const formData = await request.formData(); const formData = await request.formData();
const file = formData.get("file") as File; const file = formData.get('file') as File;
const title = formData.get("title") as string; const title = formData.get('title') as string;
const description = formData.get("description") as string; const description = formData.get('description') as string;
const filetype = file.type.split("/")[1]; const fileRepo = new LocalFileRepository(PRIVATE_PHOTO_UPLOAD_DIR);
const fileName = `${randomUUID()}.${filetype}`; const { fileName, filePath } = await fileRepo.saveFile(file);
const fileLocation = join(PRIVATE_PHOTO_UPLOAD_DIR, fileName);
const fileContentBuffer = await file.arrayBuffer();
await writeFile(fileLocation, Buffer.from(fileContentBuffer));
await locals.prisma.photoPost.create({ await locals.prisma.photoPost.create({
data: { data: {
fileName, fileName,
filePath,
title, title,
description, description,
createdAt: new Date(), 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"> <script lang="ts">
import type { PhotoPost } from "../../../../generated/prisma/client.js"; import type { PhotoPost } from "../../../../generated/prisma/client.js";
import FeedItem from "./FeedItem.svelte";
const { photos }: { photos: PhotoPost[] } = $props(); const { photos }: { photos: PhotoPost[] } = $props();
</script> </script>
<ul class="photo-feed"> <ul class="photo-feed">
{#each photos as photo, id} {#each photos as photo, id}
<li class="item"> <FeedItem {photo} />
<img width="250" src={`/image/${photo.fileName}`} alt={photo.title} />
<p>{photo.title}</p>
</li>
{/each} {/each}
</ul> </ul>
@ -21,15 +19,4 @@
list-style: none; list-style: none;
} }
.item {
display: flex;
flex-direction: column;
justify-content: center;
}
.item img {
width: 250px;
height: auto;
height: fit-content;
}
</style> </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 () => { export const GET = async () => {
const now = new Date() const now = new Date();
const body = controller.getSunriseSunsetPhotoForDate(now) const body = controller.getSunriseSunsetPhotoForDate(now);
const response = { const response = {
status: 200, status: 200,
body, 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 { it, describe, expect, beforeAll } from 'vitest';
import { import { type ISunriseSunsetController, SunriseSunsetController } from './SunriseSunsetController';
type ISunriseSunsetController,
SunriseSunsetController,
} from './SunriseSunsetController'
describe('SunriseSunsetController', () => { describe('SunriseSunsetController', () => {
let controller: ISunriseSunsetController let controller: ISunriseSunsetController;
beforeAll(() => { beforeAll(() => {
controller = new SunriseSunsetController() controller = new SunriseSunsetController();
}) });
it(`Should return a known photo for a known date`, () => { it(`Should return a known photo for a known date`, () => {
// GIVEN // GIVEN
const aKnownDate = new Date('2023-01-24T14:00Z') const aKnownDate = new Date('2023-01-24T14:00Z');
// WHEN // WHEN
const photo = controller.getSunriseSunsetPhotoForDate(aKnownDate) const photo = controller.getSunriseSunsetPhotoForDate(aKnownDate);
// THEN // THEN
expect(photo).toStrictEqual({ 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', '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', sunrise_or_sunset: 'sunrise',
}, },
}) });
}) });
it(`should return null when there is no photo for the day`, () => { it(`should return null when there is no photo for the day`, () => {
// GIVEN // GIVEN
const aDateWithoutPhoto = new Date('2020-01-01T00:00Z') const aDateWithoutPhoto = new Date('2020-01-01T00:00Z');
// WHEN // WHEN
const photo = controller.getSunriseSunsetPhotoForDate(aDateWithoutPhoto) const photo = controller.getSunriseSunsetPhotoForDate(aDateWithoutPhoto);
// THEN // THEN
expect(photo).toBeNull() expect(photo).toBeNull();
}) });
}) });

View file

@ -1,28 +1,28 @@
import data from './data.json' import data from './data.json';
import { format as formatDate } from 'date-fns' import { format as formatDate } from 'date-fns';
type Daytime = 'sunrise' | 'sunset' type Daytime = 'sunrise' | 'sunset';
interface DailyPhoto { interface DailyPhoto {
date: string // e.g. "2023-01-24" date: string; // e.g. "2023-01-24"
photo: { photo: {
id: string id: string;
description: string description: string;
username: string username: string;
username_url: string username_url: string;
small_url: string small_url: string;
sunrise_or_sunset: Daytime sunrise_or_sunset: Daytime;
} };
} }
export interface ISunriseSunsetController { export interface ISunriseSunsetController {
getSunriseSunsetPhotoForDate(date: Date): DailyPhoto | null getSunriseSunsetPhotoForDate(date: Date): DailyPhoto | null;
} }
export class SunriseSunsetController implements ISunriseSunsetController { export class SunriseSunsetController implements ISunriseSunsetController {
private data: DailyPhoto[] = data.photos as any private data: DailyPhoto[] = data.photos as any;
getSunriseSunsetPhotoForDate(date) { 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 }) => { export const GET = async ({ url }) => {
try { try {
return json({ return json({
wainwrights wainwrights,
}); });
} catch (error) { } catch (error) {
console.error({ error: JSON.stringify(error) }); console.error({ error: JSON.stringify(error) });
return json( 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 BlogItem,
type BlogPostListItem, type BlogPostListItem,
type BookReviewListItem, type BookReviewListItem,
} from "$lib/blog/BlogController.js"; } from '$lib/blog/BlogController.js';
import type { BookReview } from "$lib/blog/BookReview.js"; import type { BookReview } from '$lib/blog/BookReview.js';
import type { Load } from "@sveltejs/kit"; import type { Load } from '@sveltejs/kit';
import { differenceInCalendarDays, getYear } from "date-fns"; import { differenceInCalendarDays, getYear } from 'date-fns';
export const prerender = true; export const prerender = true;
@ -24,13 +24,13 @@ export const load: Load = async ({}) => {
const numberOfPosts = posts.length; const numberOfPosts = posts.length;
const firstPost = posts[numberOfPosts - 1]; const firstPost = posts[numberOfPosts - 1];
const numberOfBlogPostsThisYear: number = posts.filter( const numberOfBlogPostsThisYear: number = posts.filter(
(post) => getYear(new Date(post.date)) === currentYear, (post) => getYear(new Date(post.date)) === currentYear
).length; ).length;
const postsGroupedByMonth = posts.reduce((grouped, post) => { const postsGroupedByMonth = posts.reduce((grouped, post) => {
const yearDate = Intl.DateTimeFormat("en-gb", { const yearDate = Intl.DateTimeFormat('en-gb', {
year: "numeric", year: 'numeric',
month: "long", month: 'long',
}).format(new Date(post.date)); }).format(new Date(post.date));
const index = grouped.findIndex((entry) => entry.yearDate === yearDate); const index = grouped.findIndex((entry) => entry.yearDate === yearDate);

View file

@ -1,8 +1,8 @@
import { access, readFile } from "node:fs/promises"; import { access, readFile } from 'node:fs/promises';
import { constants } from "node:fs"; import { constants } from 'node:fs';
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private"; import { PRIVATE_PHOTO_UPLOAD_DIR } from '$env/static/private';
import * as path from "node:path"; import * as path from 'node:path';
import { error, type ServerLoad } from "@sveltejs/kit"; import { error, type ServerLoad } from '@sveltejs/kit';
export const GET: ServerLoad = async ({ params, locals }) => { export const GET: ServerLoad = async ({ params, locals }) => {
const { filename } = params; const { filename } = params;
@ -14,12 +14,13 @@ export const GET: ServerLoad = async ({ params, locals }) => {
.catch(() => false); .catch(() => false);
if (!fileExists) { 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 file = await readFile(proposedFilePath);
const fileExt = path.extname(filename); 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({}) => { export const load: PageServerLoad = async ({}) => {
return { return {};
};
}
}

View file

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

View file

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

View file

@ -1,3 +1,105 @@
/* PrismJS 1.29.0 /* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism-solarizedlight&languages=markup+css+clike+javascript */ 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-face {
font-family: "FivoSansModern-Regular"; font-family: 'FivoSansModern-Regular';
src: url("/FivoSansModern-Regular.otf"); src: url('/FivoSansModern-Regular.otf');
font-display: swap; font-display: swap;
} }
@ -21,19 +21,24 @@
--gray-900: #212529; --gray-900: #212529;
--gray-950: #1a1e23; --gray-950: #1a1e23;
--gray-1000: #0a0c0e; --gray-1000: #0a0c0e;
--colour-danger: red;
--font-family-mono: monospace; --font-family-mono: monospace;
--font-family-title: "FivoSansModern-Regular", sans-serif; --font-family-title: 'FivoSansModern-Regular', sans-serif;
--font-family-sans: --font-family-sans:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
"Segoe UI Symbol", "Noto Color Emoji"; --font-family-serif: Georgia, Cambria, 'Times New Roman', Times, serif;
--font-family-serif: Georgia, Cambria, "Times New Roman", Times, serif;
--line-height: 120%; --line-height: 120%;
--line-height-sm: 120%; --line-height-sm: 120%;
--line-height-md: 140%; --line-height-md: 140%;
--line-height-lg: 145%; --line-height-lg: 145%;
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--font-size-xs: 11px; --font-size-xs: 11px;
--font-size-sm: 13px; --font-size-sm: 13px;
--font-size-base: 1rem; --font-size-base: 1rem;
@ -85,9 +90,7 @@ body {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: calc( min-height: calc(100vh - var(--navbar-height) - calc(2 * var(--container-padding)));
100vh - var(--navbar-height) - calc(2 * var(--container-padding))
);
padding: var(--container-padding); padding: var(--container-padding);
} }
@ -177,6 +180,11 @@ ol {
text-decoration: var(--btn-text-decoration); text-decoration: var(--btn-text-decoration);
} }
.thomaswilson-button.danger {
color: var(--colour-danger);
border: 1px solid var(--colour-danger);
}
.thomaswilson-button:hover { .thomaswilson-button:hover {
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
@ -200,7 +208,7 @@ img {
} }
a::after { a::after {
content: url("/assets/icons/link.svg"); content: url('/assets/icons/link.svg');
margin-left: 3px; margin-left: 3px;
} }
@ -210,7 +218,17 @@ sup a::after {
} }
a.no-icon::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; display: none;
} }
@ -260,3 +278,96 @@ a.no-icon::after {
color: var(--colour-scheme-text); color: var(--colour-scheme-text);
background-color: var(--colour-scheme-bg); 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 adapter from '@sveltejs/adapter-node';
import preprocess from "svelte-preprocess"; import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
extensions: [".svelte", ".md"], extensions: ['.svelte', '.md'],
// Consult https://github.com/sveltejs/svelte-preprocess // Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors // for more information about preprocessors
preprocess: [preprocess()], preprocess: [preprocess()],
@ -11,13 +11,13 @@ const config = {
kit: { kit: {
adapter: adapter({ split: false }), adapter: adapter({ split: false }),
alias: { alias: {
$lib: "/src/lib", $lib: '/src/lib',
$srcPrisma: "/src/prisma", $srcPrisma: '/src/prisma',
$generatedPrisma: "/generated/prisma/*", $generatedPrisma: '/generated/prisma/*',
}, },
env: { env: {
publicPrefix: "PUBLIC_", publicPrefix: 'PUBLIC_',
privatePrefix: "PRIVATE_", privatePrefix: 'PRIVATE_',
}, },
}, },
}; };

View file

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

View file

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