feat: Update and delete an image
This commit is contained in:
parent
61bf7f6162
commit
933278ec98
52 changed files with 8844 additions and 5755 deletions
|
|
@ -1,20 +1,20 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript')
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript'),
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,6 +36,50 @@ export type DateTimeFilter<$PrismaModel = never> = {
|
|||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
|
||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type StringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type StringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type SortOrderInput = {
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
|
|
@ -66,6 +110,54 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
|||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
|
|
@ -88,6 +180,45 @@ export type NestedDateTimeFilter<$PrismaModel = never> = {
|
|||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type NestedStringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
|
|
@ -129,4 +260,63 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
|||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
|
|||
"clientVersion": "7.4.2",
|
||||
"engineVersion": "94a226be1cf2967af2541cca5529f0f7ba866919",
|
||||
"activeProvider": "sqlite",
|
||||
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel PhotoPost {\n id Int @id @default(autoincrement())\n createdAt DateTime @default(now())\n}\n",
|
||||
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel PhotoPost {\n id Int @id @default(autoincrement())\n createdAt DateTime @default(now())\n deletedAt DateTime?\n publishedAt DateTime?\n filePath String\n fileName String\n title String\n description String?\n}\n",
|
||||
"runtimeDataModel": {
|
||||
"models": {},
|
||||
"enums": {},
|
||||
|
|
@ -32,10 +32,10 @@ const config: runtime.GetPrismaClientConfig = {
|
|||
}
|
||||
}
|
||||
|
||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"PhotoPost\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"PhotoPost\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"deletedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"publishedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"filePath\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fileName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||
config.parameterizationSchema = {
|
||||
strings: JSON.parse("[\"where\",\"PhotoPost.findUnique\",\"PhotoPost.findUniqueOrThrow\",\"orderBy\",\"cursor\",\"PhotoPost.findFirst\",\"PhotoPost.findFirstOrThrow\",\"PhotoPost.findMany\",\"data\",\"PhotoPost.createOne\",\"PhotoPost.createMany\",\"PhotoPost.createManyAndReturn\",\"PhotoPost.updateOne\",\"PhotoPost.updateMany\",\"PhotoPost.updateManyAndReturn\",\"create\",\"update\",\"PhotoPost.upsertOne\",\"PhotoPost.deleteOne\",\"PhotoPost.deleteMany\",\"having\",\"_count\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"PhotoPost.groupBy\",\"PhotoPost.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"createdAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"set\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"),
|
||||
graph: "KwsQBRwAACIAMB0AAAQAEB4AACIAMB8CAAAAASBAACQAIQEAAAABACABAAAAAQAgBRwAACIAMB0AAAQAEB4AACIAMB8CACMAISBAACQAIQADAAAABAAgAwAABQAwBAAAAQAgAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACACHwIAAAABIEAAAAABAQgAAAkAIAIfAgAAAAEgQAAAAAEBCAAACwAwAQgAAAsAMAIfAgArACEgQAAqACECAAAAAQAgCAAADgAgAh8CACsAISBAACoAIQIAAAAEACAIAAAQACACAAAABAAgCAAAEAAgAwAAAAEAIA8AAAkAIBAAAA4AIAEAAAABACABAAAABAAgBRUAACUAIBYAACYAIBcAACkAIBgAACgAIBkAACcAIAUcAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEDAAAABAAgAwAAFgAwFAAAFwAgAwAAAAQAIAMAAAUAMAQAAAEAIAUcAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACENFQAAHgAgFgAAIQAgFwAAHgAgGAAAHgAgGQAAHgAgIQIAAAABIgIAAAAEIwIAAAAEJAIAAAABJQIAAAABJgIAAAABJwIAAAABKAIAIAAhCxUAAB4AIBgAAB8AIBkAAB8AICFAAAAAASJAAAAABCNAAAAABCRAAAAAASVAAAAAASZAAAAAASdAAAAAAShAAB0AIQsVAAAeACAYAAAfACAZAAAfACAhQAAAAAEiQAAAAAQjQAAAAAQkQAAAAAElQAAAAAEmQAAAAAEnQAAAAAEoQAAdACEIIQIAAAABIgIAAAAEIwIAAAAEJAIAAAABJQIAAAABJgIAAAABJwIAAAABKAIAHgAhCCFAAAAAASJAAAAABCNAAAAABCRAAAAAASVAAAAAASZAAAAAASdAAAAAAShAAB8AIQ0VAAAeACAWAAAhACAXAAAeACAYAAAeACAZAAAeACAhAgAAAAEiAgAAAAQjAgAAAAQkAgAAAAElAgAAAAEmAgAAAAEnAgAAAAEoAgAgACEIIQgAAAABIggAAAAEIwgAAAAEJAgAAAABJQgAAAABJggAAAABJwgAAAABKAgAIQAhBRwAACIAMB0AAAQAEB4AACIAMB8CACMAISBAACQAIQghAgAAAAEiAgAAAAQjAgAAAAQkAgAAAAElAgAAAAEmAgAAAAEnAgAAAAEoAgAeACEIIUAAAAABIkAAAAAEI0AAAAAEJEAAAAABJUAAAAABJkAAAAABJ0AAAAABKEAAHwAhAAAAAAABKUAAAAABBSkCAAAAASoCAAAAASsCAAAAASwCAAAAAS0CAAAAAQAAAAAFFQAGFgAHFwAIGAAJGQAKAAAAAAAFFQAGFgAHFwAIGAAJGQAKAQIBAgMBBQYBBgcBBwgBCQoBCgwCCw0DDA8BDRECDhIEERMBEhQBExUCGhgFGxkL"
|
||||
strings: JSON.parse("[\"where\",\"PhotoPost.findUnique\",\"PhotoPost.findUniqueOrThrow\",\"orderBy\",\"cursor\",\"PhotoPost.findFirst\",\"PhotoPost.findFirstOrThrow\",\"PhotoPost.findMany\",\"data\",\"PhotoPost.createOne\",\"PhotoPost.createMany\",\"PhotoPost.createManyAndReturn\",\"PhotoPost.updateOne\",\"PhotoPost.updateMany\",\"PhotoPost.updateManyAndReturn\",\"create\",\"update\",\"PhotoPost.upsertOne\",\"PhotoPost.deleteOne\",\"PhotoPost.deleteMany\",\"having\",\"_count\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"PhotoPost.groupBy\",\"PhotoPost.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"createdAt\",\"deletedAt\",\"publishedAt\",\"filePath\",\"fileName\",\"title\",\"description\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"contains\",\"startsWith\",\"endsWith\",\"not\",\"set\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"),
|
||||
graph: "PAsQCxwAACwAMB0AAAQAEB4AACwAMB8CAAAAASBAAC4AISFAAC8AISJAAC8AISMBADAAISQBADAAISUBADAAISYBADEAIQEAAAABACABAAAAAQAgCxwAACwAMB0AAAQAEB4AACwAMB8CAC0AISBAAC4AISFAAC8AISJAAC8AISMBADAAISQBADAAISUBADAAISYBADEAIQMhAAAyACAiAAAyACAmAAAyACADAAAABAAgAwAABQAwBAAAAQAgAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACAIHwIAAAABIEAAAAABIUAAAAABIkAAAAABIwEAAAABJAEAAAABJQEAAAABJgEAAAABAQgAAAkAIAgfAgAAAAEgQAAAAAEhQAAAAAEiQAAAAAEjAQAAAAEkAQAAAAElAQAAAAEmAQAAAAEBCAAACwAwAQgAAAsAMAgfAgA8ACEgQAA4ACEhQAA5ACEiQAA5ACEjAQA6ACEkAQA6ACElAQA6ACEmAQA7ACECAAAAAQAgCAAADgAgCB8CADwAISBAADgAISFAADkAISJAADkAISMBADoAISQBADoAISUBADoAISYBADsAIQIAAAAEACAIAAAQACACAAAABAAgCAAAEAAgAwAAAAEAIA8AAAkAIBAAAA4AIAEAAAABACABAAAABAAgCBUAADMAIBYAADQAIBcAADcAIBgAADYAIBkAADUAICEAADIAICIAADIAICYAADIAIAscAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEhQAAdACEiQAAdACEjAQAeACEkAQAeACElAQAeACEmAQAfACEDAAAABAAgAwAAFgAwFAAAFwAgAwAAAAQAIAMAAAUAMAQAAAEAIAscAAAaADAdAAAXABAeAAAaADAfAgAbACEgQAAcACEhQAAdACEiQAAdACEjAQAeACEkAQAeACElAQAeACEmAQAfACENFQAAJAAgFgAAKwAgFwAAJAAgGAAAJAAgGQAAJAAgJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAKgAhCxUAACQAIBgAACkAIBkAACkAICdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACgAIQsVAAAhACAYAAAnACAZAAAnACAnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAmACEOFQAAJAAgGAAAJQAgGQAAJQAgJwEAAAABKAEAAAAEKQEAAAAEKgEAAAABKwEAAAABLAEAAAABLQEAAAABLgEAAAABLwEAAAABMAEAAAABMQEAIwAhDhUAACEAIBgAACIAIBkAACIAICcBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACAAIQ4VAAAhACAYAAAiACAZAAAiACAnAQAAAAEoAQAAAAUpAQAAAAUqAQAAAAErAQAAAAEsAQAAAAEtAQAAAAEuAQAAAAEvAQAAAAEwAQAAAAExAQAgACEIJwIAAAABKAIAAAAFKQIAAAAFKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAIQAhCycBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACIAIQ4VAAAkACAYAAAlACAZAAAlACAnAQAAAAEoAQAAAAQpAQAAAAQqAQAAAAErAQAAAAEsAQAAAAEtAQAAAAEuAQAAAAEvAQAAAAEwAQAAAAExAQAjACEIJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAJAAhCycBAAAAASgBAAAABCkBAAAABCoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACUAIQsVAAAhACAYAAAnACAZAAAnACAnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAmACEIJ0AAAAABKEAAAAAFKUAAAAAFKkAAAAABK0AAAAABLEAAAAABLUAAAAABMUAAJwAhCxUAACQAIBgAACkAIBkAACkAICdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACgAIQgnQAAAAAEoQAAAAAQpQAAAAAQqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAApACENFQAAJAAgFgAAKwAgFwAAJAAgGAAAJAAgGQAAJAAgJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAKgAhCCcIAAAAASgIAAAABCkIAAAABCoIAAAAASsIAAAAASwIAAAAAS0IAAAAATEIACsAIQscAAAsADAdAAAEABAeAAAsADAfAgAtACEgQAAuACEhQAAvACEiQAAvACEjAQAwACEkAQAwACElAQAwACEmAQAxACEIJwIAAAABKAIAAAAEKQIAAAAEKgIAAAABKwIAAAABLAIAAAABLQIAAAABMQIAJAAhCCdAAAAAAShAAAAABClAAAAABCpAAAAAAStAAAAAASxAAAAAAS1AAAAAATFAACkAIQgnQAAAAAEoQAAAAAUpQAAAAAUqQAAAAAErQAAAAAEsQAAAAAEtQAAAAAExQAAnACELJwEAAAABKAEAAAAEKQEAAAAEKgEAAAABKwEAAAABLAEAAAABLQEAAAABLgEAAAABLwEAAAABMAEAAAABMQEAJQAhCycBAAAAASgBAAAABSkBAAAABSoBAAAAASsBAAAAASwBAAAAAS0BAAAAAS4BAAAAAS8BAAAAATABAAAAATEBACIAIQAAAAAAAAEyQAAAAAEBMkAAAAABATIBAAAAAQEyAQAAAAEFMgIAAAABMwIAAAABNAIAAAABNQIAAAABNgIAAAABAAAAAAUVAAYWAAcXAAgYAAkZAAoAAAAAAAUVAAYWAAcXAAgYAAkZAAoBAgECAwEFBgEGBwEHCAEJCgEKDAILDQMMDwENEQIOEgQREwESFAETFQIaGAUbGQs"
|
||||
}
|
||||
|
||||
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||
|
|
|
|||
|
|
@ -516,7 +516,13 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
|
|||
|
||||
export const PhotoPostScalarFieldEnum = {
|
||||
id: 'id',
|
||||
createdAt: 'createdAt'
|
||||
createdAt: 'createdAt',
|
||||
deletedAt: 'deletedAt',
|
||||
publishedAt: 'publishedAt',
|
||||
filePath: 'filePath',
|
||||
fileName: 'fileName',
|
||||
title: 'title',
|
||||
description: 'description'
|
||||
} as const
|
||||
|
||||
export type PhotoPostScalarFieldEnum = (typeof PhotoPostScalarFieldEnum)[keyof typeof PhotoPostScalarFieldEnum]
|
||||
|
|
@ -530,6 +536,14 @@ export const SortOrder = {
|
|||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Field references
|
||||
|
|
@ -550,6 +564,13 @@ export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel
|
|||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'String'
|
||||
*/
|
||||
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Float'
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -69,7 +69,13 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
|
|||
|
||||
export const PhotoPostScalarFieldEnum = {
|
||||
id: 'id',
|
||||
createdAt: 'createdAt'
|
||||
createdAt: 'createdAt',
|
||||
deletedAt: 'deletedAt',
|
||||
publishedAt: 'publishedAt',
|
||||
filePath: 'filePath',
|
||||
fileName: 'fileName',
|
||||
title: 'title',
|
||||
description: 'description'
|
||||
} as const
|
||||
|
||||
export type PhotoPostScalarFieldEnum = (typeof PhotoPostScalarFieldEnum)[keyof typeof PhotoPostScalarFieldEnum]
|
||||
|
|
@ -82,3 +88,11 @@ export const SortOrder = {
|
|||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
|
|
|
|||
|
|
@ -37,16 +37,34 @@ export type PhotoPostSumAggregateOutputType = {
|
|||
export type PhotoPostMinAggregateOutputType = {
|
||||
id: number | null
|
||||
createdAt: Date | null
|
||||
deletedAt: Date | null
|
||||
publishedAt: Date | null
|
||||
filePath: string | null
|
||||
fileName: string | null
|
||||
title: string | null
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export type PhotoPostMaxAggregateOutputType = {
|
||||
id: number | null
|
||||
createdAt: Date | null
|
||||
deletedAt: Date | null
|
||||
publishedAt: Date | null
|
||||
filePath: string | null
|
||||
fileName: string | null
|
||||
title: string | null
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export type PhotoPostCountAggregateOutputType = {
|
||||
id: number
|
||||
createdAt: number
|
||||
deletedAt: number
|
||||
publishedAt: number
|
||||
filePath: number
|
||||
fileName: number
|
||||
title: number
|
||||
description: number
|
||||
_all: number
|
||||
}
|
||||
|
||||
|
|
@ -62,16 +80,34 @@ export type PhotoPostSumAggregateInputType = {
|
|||
export type PhotoPostMinAggregateInputType = {
|
||||
id?: true
|
||||
createdAt?: true
|
||||
deletedAt?: true
|
||||
publishedAt?: true
|
||||
filePath?: true
|
||||
fileName?: true
|
||||
title?: true
|
||||
description?: true
|
||||
}
|
||||
|
||||
export type PhotoPostMaxAggregateInputType = {
|
||||
id?: true
|
||||
createdAt?: true
|
||||
deletedAt?: true
|
||||
publishedAt?: true
|
||||
filePath?: true
|
||||
fileName?: true
|
||||
title?: true
|
||||
description?: true
|
||||
}
|
||||
|
||||
export type PhotoPostCountAggregateInputType = {
|
||||
id?: true
|
||||
createdAt?: true
|
||||
deletedAt?: true
|
||||
publishedAt?: true
|
||||
filePath?: true
|
||||
fileName?: true
|
||||
title?: true
|
||||
description?: true
|
||||
_all?: true
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +200,12 @@ export type PhotoPostGroupByArgs<ExtArgs extends runtime.Types.Extensions.Intern
|
|||
export type PhotoPostGroupByOutputType = {
|
||||
id: number
|
||||
createdAt: Date
|
||||
deletedAt: Date | null
|
||||
publishedAt: Date | null
|
||||
filePath: string
|
||||
fileName: string
|
||||
title: string
|
||||
description: string | null
|
||||
_count: PhotoPostCountAggregateOutputType | null
|
||||
_avg: PhotoPostAvgAggregateOutputType | null
|
||||
_sum: PhotoPostSumAggregateOutputType | null
|
||||
|
|
@ -192,11 +234,23 @@ export type PhotoPostWhereInput = {
|
|||
NOT?: Prisma.PhotoPostWhereInput | Prisma.PhotoPostWhereInput[]
|
||||
id?: Prisma.IntFilter<"PhotoPost"> | number
|
||||
createdAt?: Prisma.DateTimeFilter<"PhotoPost"> | Date | string
|
||||
deletedAt?: Prisma.DateTimeNullableFilter<"PhotoPost"> | Date | string | null
|
||||
publishedAt?: Prisma.DateTimeNullableFilter<"PhotoPost"> | Date | string | null
|
||||
filePath?: Prisma.StringFilter<"PhotoPost"> | string
|
||||
fileName?: Prisma.StringFilter<"PhotoPost"> | string
|
||||
title?: Prisma.StringFilter<"PhotoPost"> | string
|
||||
description?: Prisma.StringNullableFilter<"PhotoPost"> | string | null
|
||||
}
|
||||
|
||||
export type PhotoPostOrderByWithRelationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
deletedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
filePath?: Prisma.SortOrder
|
||||
fileName?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type PhotoPostWhereUniqueInput = Prisma.AtLeast<{
|
||||
|
|
@ -205,11 +259,23 @@ export type PhotoPostWhereUniqueInput = Prisma.AtLeast<{
|
|||
OR?: Prisma.PhotoPostWhereInput[]
|
||||
NOT?: Prisma.PhotoPostWhereInput | Prisma.PhotoPostWhereInput[]
|
||||
createdAt?: Prisma.DateTimeFilter<"PhotoPost"> | Date | string
|
||||
deletedAt?: Prisma.DateTimeNullableFilter<"PhotoPost"> | Date | string | null
|
||||
publishedAt?: Prisma.DateTimeNullableFilter<"PhotoPost"> | Date | string | null
|
||||
filePath?: Prisma.StringFilter<"PhotoPost"> | string
|
||||
fileName?: Prisma.StringFilter<"PhotoPost"> | string
|
||||
title?: Prisma.StringFilter<"PhotoPost"> | string
|
||||
description?: Prisma.StringNullableFilter<"PhotoPost"> | string | null
|
||||
}, "id">
|
||||
|
||||
export type PhotoPostOrderByWithAggregationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
deletedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
filePath?: Prisma.SortOrder
|
||||
fileName?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
_count?: Prisma.PhotoPostCountOrderByAggregateInput
|
||||
_avg?: Prisma.PhotoPostAvgOrderByAggregateInput
|
||||
_max?: Prisma.PhotoPostMaxOrderByAggregateInput
|
||||
|
|
@ -223,43 +289,97 @@ export type PhotoPostScalarWhereWithAggregatesInput = {
|
|||
NOT?: Prisma.PhotoPostScalarWhereWithAggregatesInput | Prisma.PhotoPostScalarWhereWithAggregatesInput[]
|
||||
id?: Prisma.IntWithAggregatesFilter<"PhotoPost"> | number
|
||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"PhotoPost"> | Date | string
|
||||
deletedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"PhotoPost"> | Date | string | null
|
||||
publishedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"PhotoPost"> | Date | string | null
|
||||
filePath?: Prisma.StringWithAggregatesFilter<"PhotoPost"> | string
|
||||
fileName?: Prisma.StringWithAggregatesFilter<"PhotoPost"> | string
|
||||
title?: Prisma.StringWithAggregatesFilter<"PhotoPost"> | string
|
||||
description?: Prisma.StringNullableWithAggregatesFilter<"PhotoPost"> | string | null
|
||||
}
|
||||
|
||||
export type PhotoPostCreateInput = {
|
||||
createdAt?: Date | string
|
||||
deletedAt?: Date | string | null
|
||||
publishedAt?: Date | string | null
|
||||
filePath: string
|
||||
fileName: string
|
||||
title: string
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
export type PhotoPostUncheckedCreateInput = {
|
||||
id?: number
|
||||
createdAt?: Date | string
|
||||
deletedAt?: Date | string | null
|
||||
publishedAt?: Date | string | null
|
||||
filePath: string
|
||||
fileName: string
|
||||
title: string
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
export type PhotoPostUpdateInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
publishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
filePath?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
fileName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type PhotoPostUncheckedUpdateInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
publishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
filePath?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
fileName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type PhotoPostCreateManyInput = {
|
||||
id?: number
|
||||
createdAt?: Date | string
|
||||
deletedAt?: Date | string | null
|
||||
publishedAt?: Date | string | null
|
||||
filePath: string
|
||||
fileName: string
|
||||
title: string
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
export type PhotoPostUpdateManyMutationInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
publishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
filePath?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
fileName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type PhotoPostUncheckedUpdateManyInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
publishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
filePath?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
fileName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
}
|
||||
|
||||
export type PhotoPostCountOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
deletedAt?: Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrder
|
||||
filePath?: Prisma.SortOrder
|
||||
fileName?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type PhotoPostAvgOrderByAggregateInput = {
|
||||
|
|
@ -269,11 +389,23 @@ export type PhotoPostAvgOrderByAggregateInput = {
|
|||
export type PhotoPostMaxOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
deletedAt?: Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrder
|
||||
filePath?: Prisma.SortOrder
|
||||
fileName?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type PhotoPostMinOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
deletedAt?: Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrder
|
||||
filePath?: Prisma.SortOrder
|
||||
fileName?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type PhotoPostSumOrderByAggregateInput = {
|
||||
|
|
@ -284,6 +416,18 @@ export type DateTimeFieldUpdateOperationsInput = {
|
|||
set?: Date | string
|
||||
}
|
||||
|
||||
export type NullableDateTimeFieldUpdateOperationsInput = {
|
||||
set?: Date | string | null
|
||||
}
|
||||
|
||||
export type StringFieldUpdateOperationsInput = {
|
||||
set?: string
|
||||
}
|
||||
|
||||
export type NullableStringFieldUpdateOperationsInput = {
|
||||
set?: string | null
|
||||
}
|
||||
|
||||
export type IntFieldUpdateOperationsInput = {
|
||||
set?: number
|
||||
increment?: number
|
||||
|
|
@ -297,24 +441,48 @@ export type IntFieldUpdateOperationsInput = {
|
|||
export type PhotoPostSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
createdAt?: boolean
|
||||
deletedAt?: boolean
|
||||
publishedAt?: boolean
|
||||
filePath?: boolean
|
||||
fileName?: boolean
|
||||
title?: boolean
|
||||
description?: boolean
|
||||
}, ExtArgs["result"]["photoPost"]>
|
||||
|
||||
export type PhotoPostSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
createdAt?: boolean
|
||||
deletedAt?: boolean
|
||||
publishedAt?: boolean
|
||||
filePath?: boolean
|
||||
fileName?: boolean
|
||||
title?: boolean
|
||||
description?: boolean
|
||||
}, ExtArgs["result"]["photoPost"]>
|
||||
|
||||
export type PhotoPostSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
createdAt?: boolean
|
||||
deletedAt?: boolean
|
||||
publishedAt?: boolean
|
||||
filePath?: boolean
|
||||
fileName?: boolean
|
||||
title?: boolean
|
||||
description?: boolean
|
||||
}, ExtArgs["result"]["photoPost"]>
|
||||
|
||||
export type PhotoPostSelectScalar = {
|
||||
id?: boolean
|
||||
createdAt?: boolean
|
||||
deletedAt?: boolean
|
||||
publishedAt?: boolean
|
||||
filePath?: boolean
|
||||
fileName?: boolean
|
||||
title?: boolean
|
||||
description?: boolean
|
||||
}
|
||||
|
||||
export type PhotoPostOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "createdAt", ExtArgs["result"]["photoPost"]>
|
||||
export type PhotoPostOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "createdAt" | "deletedAt" | "publishedAt" | "filePath" | "fileName" | "title" | "description", ExtArgs["result"]["photoPost"]>
|
||||
|
||||
export type $PhotoPostPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
name: "PhotoPost"
|
||||
|
|
@ -322,6 +490,12 @@ export type $PhotoPostPayload<ExtArgs extends runtime.Types.Extensions.InternalA
|
|||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: number
|
||||
createdAt: Date
|
||||
deletedAt: Date | null
|
||||
publishedAt: Date | null
|
||||
filePath: string
|
||||
fileName: string
|
||||
title: string
|
||||
description: string | null
|
||||
}, ExtArgs["result"]["photoPost"]>
|
||||
composites: {}
|
||||
}
|
||||
|
|
@ -747,6 +921,12 @@ export interface Prisma__PhotoPostClient<T, Null = never, ExtArgs extends runtim
|
|||
export interface PhotoPostFieldRefs {
|
||||
readonly id: Prisma.FieldRef<"PhotoPost", 'Int'>
|
||||
readonly createdAt: Prisma.FieldRef<"PhotoPost", 'DateTime'>
|
||||
readonly deletedAt: Prisma.FieldRef<"PhotoPost", 'DateTime'>
|
||||
readonly publishedAt: Prisma.FieldRef<"PhotoPost", 'DateTime'>
|
||||
readonly filePath: Prisma.FieldRef<"PhotoPost", 'String'>
|
||||
readonly fileName: Prisma.FieldRef<"PhotoPost", 'String'>
|
||||
readonly title: Prisma.FieldRef<"PhotoPost", 'String'>
|
||||
readonly description: Prisma.FieldRef<"PhotoPost", 'String'>
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -941,7 +1121,7 @@ export type PhotoPostCreateArgs<ExtArgs extends runtime.Types.Extensions.Interna
|
|||
/**
|
||||
* The data needed to create a PhotoPost.
|
||||
*/
|
||||
data?: Prisma.XOR<Prisma.PhotoPostCreateInput, Prisma.PhotoPostUncheckedCreateInput>
|
||||
data: Prisma.XOR<Prisma.PhotoPostCreateInput, Prisma.PhotoPostUncheckedCreateInput>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
140
package.json
140
package.json
|
|
@ -1,72 +1,72 @@
|
|||
{
|
||||
"name": "thomaswilson-sveltekit",
|
||||
"license": "UNLICENSED",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:deploy": "prisma migrate deploy",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --config ./prettierrc --write --plugin-search-dir=. .",
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.51.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/leaflet": "^1.9.15",
|
||||
"@types/node": "^25.3.2",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
||||
"@typescript-eslint/parser": "^8.55.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-svelte": "^3.15.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"svelte": "^5.50.3",
|
||||
"svelte-check": "^4.3.6",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.7",
|
||||
"vitest": "^3.0.8"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@immich/sdk": "^2.5.6",
|
||||
"@prisma/adapter-better-sqlite3": "^7.4.2",
|
||||
"@prisma/client": "^7.4.2",
|
||||
"@sveltejs/adapter-node": "^5.5.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"canvas-dither": "^1.0.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"feed": "^4.2.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"just-shuffle": "^4.2.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prisma": "^7.4.2",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark": "^15.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"sass": "^1.85.1",
|
||||
"strip-markdown": "^6.0.0",
|
||||
"unified": "^11.0.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
"name": "thomaswilson-sveltekit",
|
||||
"license": "UNLICENSED",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:deploy": "prisma migrate deploy",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --config ./prettierrc --write --plugin-search-dir=. .",
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.51.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/leaflet": "^1.9.15",
|
||||
"@types/node": "^25.3.2",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
||||
"@typescript-eslint/parser": "^8.55.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-svelte": "^3.15.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"svelte": "^5.50.3",
|
||||
"svelte-check": "^4.3.6",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.7",
|
||||
"vitest": "^3.0.8"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@immich/sdk": "^2.5.6",
|
||||
"@prisma/adapter-better-sqlite3": "^7.4.2",
|
||||
"@prisma/client": "^7.4.2",
|
||||
"@sveltejs/adapter-node": "^5.5.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"canvas-dither": "^1.0.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"feed": "^4.2.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"just-shuffle": "^4.2.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prisma": "^7.4.2",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark": "^15.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"sass": "^1.85.1",
|
||||
"strip-markdown": "^6.0.0",
|
||||
"unified": "^11.0.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10606
pnpm-lock.yaml
10606
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,14 @@
|
|||
// This file was generated by Prisma, and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
import 'dotenv/config';
|
||||
import { defineConfig } from 'prisma/config';
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "./prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"],
|
||||
},
|
||||
schema: './prisma/schema.prisma',
|
||||
migrations: {
|
||||
path: './prisma/migrations',
|
||||
},
|
||||
datasource: {
|
||||
url: process.env['DATABASE_URL'],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -8,6 +8,12 @@ datasource db {
|
|||
}
|
||||
|
||||
model PhotoPost {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
publishedAt DateTime?
|
||||
filePath String
|
||||
fileName String
|
||||
title String
|
||||
description String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,13 +44,13 @@
|
|||
method="POST"
|
||||
action="/admin/photos"
|
||||
enctype="multipart/form-data"
|
||||
class="admin-form"
|
||||
class="cms-form"
|
||||
>
|
||||
<canvas bind:this={canvasElement}></canvas>
|
||||
|
||||
<div class="field">
|
||||
<label for="title">Title</label>
|
||||
<input type="text" name="title" id="title" />
|
||||
<input type="text" name="title" id="title" required />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
|
|
@ -59,6 +59,7 @@
|
|||
type="file"
|
||||
name="file"
|
||||
accept="image/*"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export interface Authenticator {
|
||||
authenticate(password: string): boolean;
|
||||
}
|
||||
authenticate(password: string): boolean;
|
||||
}
|
||||
|
|
|
|||
52
src/lib/LocalFileRepository.ts
Normal file
52
src/lib/LocalFileRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +1,125 @@
|
|||
import {
|
||||
describe,
|
||||
it,
|
||||
beforeEach,
|
||||
afterAll,
|
||||
beforeAll,
|
||||
expect,
|
||||
afterEach,
|
||||
} from "vitest";
|
||||
import { BlogController } from "./BlogController.js";
|
||||
import { MarkdownRepository } from "./markdown-repository.js";
|
||||
import {
|
||||
exampleMarkdown,
|
||||
exampleMarkdownFrontmatter,
|
||||
} from "./test-fixtures/example-markdown.js";
|
||||
import { describe, it, beforeEach, afterAll, beforeAll, expect, afterEach } from 'vitest';
|
||||
import { BlogController } from './BlogController.js';
|
||||
import { MarkdownRepository } from './markdown-repository.js';
|
||||
import { exampleMarkdown, exampleMarkdownFrontmatter } from './test-fixtures/example-markdown.js';
|
||||
|
||||
describe(`BlogController`, () => {
|
||||
let controller: BlogController;
|
||||
|
||||
beforeEach(async () => {
|
||||
controller = await BlogController.singleton();
|
||||
});
|
||||
|
||||
describe(`Getting all posts which show up on the /blog page`, () => {
|
||||
it(`should load blogs from the content folder`, async () => {
|
||||
// GIVEN
|
||||
const blogPosts = await controller.getAllBlogPosts();
|
||||
|
||||
// WHEN
|
||||
const aKnownBlogPost = blogPosts.find(
|
||||
(post) => post.title === "Vibe Check #10",
|
||||
);
|
||||
const aKnownBookReview = blogPosts.find((post) => post.title === "After");
|
||||
const aMadeUpBlogPost = blogPosts.find(
|
||||
(post) => post.title === "Some made up blog post",
|
||||
);
|
||||
|
||||
// then
|
||||
expect(aMadeUpBlogPost).toBeUndefined();
|
||||
expect(aKnownBlogPost).not.toBeUndefined();
|
||||
expect(aKnownBookReview).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`getBlogPostBySlug`, () => {
|
||||
it(`should return null when the post doesn't exist`, async () => {
|
||||
// When
|
||||
const shouldBeNull = await controller.getBlogPostBySlug(
|
||||
"some-made-up-blog-post",
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(shouldBeNull).toBeNull();
|
||||
});
|
||||
|
||||
it(`should return the blog post when it exists`, async () => {
|
||||
// When
|
||||
const blogPost = await controller.getBlogPostBySlug(
|
||||
"2023-02-03-vibe-check-10",
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(blogPost).not.toBeNull();
|
||||
expect(blogPost.title).toBe("Vibe Check #10");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Finding content by slug`, () => {
|
||||
describe(`Finding a blog post`, () => {
|
||||
// GIVEN
|
||||
const slugForRealBlogPost = "2023-02-03-vibe-check-10";
|
||||
const slugForFakeBlogPost = "some-made-up-blog-post";
|
||||
|
||||
it(`should return null if there's no blog post with the slug`, async () => {
|
||||
// WHEN
|
||||
const blogPost =
|
||||
await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost);
|
||||
|
||||
// THEN
|
||||
expect(blogPost).toBeNull();
|
||||
});
|
||||
|
||||
it(`should return the blog post if it exists`, async () => {
|
||||
// WHEN
|
||||
const blogPost =
|
||||
await controller.getAnyKindOfContentBySlug(slugForRealBlogPost);
|
||||
|
||||
// THEN
|
||||
expect(blogPost).not.toBeNull();
|
||||
expect(blogPost.title).toBe("Vibe Check #10");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Finding a book review`, () => {
|
||||
const realSlug = "after";
|
||||
const fakeSlug = "some-made-up-book-review";
|
||||
|
||||
it(`should return null if there's no book review with the slug`, async () => {
|
||||
// WHEN
|
||||
const bookReview = await controller.getAnyKindOfContentBySlug(fakeSlug);
|
||||
|
||||
// THEN
|
||||
expect(bookReview).toBeNull();
|
||||
});
|
||||
|
||||
it(`should return the book review if it exists`, async () => {
|
||||
// WHEN
|
||||
const bookReview = await controller.getAnyKindOfContentBySlug(realSlug);
|
||||
|
||||
// THEN
|
||||
expect(bookReview).not.toBeNull();
|
||||
expect(bookReview.title).toBe("After");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Creating a new blog post as a file`, () => {
|
||||
const thisDirectory = import.meta.url
|
||||
.replace("file://", "")
|
||||
.split("/")
|
||||
.filter((part) => part !== "BlogController.test.ts")
|
||||
.join("/");
|
||||
const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`;
|
||||
let controller: BlogController;
|
||||
|
||||
beforeEach(async () => {
|
||||
controller = await BlogController.singleton();
|
||||
controller = await BlogController.singleton();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.markdownRepository.deleteBlogPostMarkdownFile(fileName);
|
||||
describe(`Getting all posts which show up on the /blog page`, () => {
|
||||
it(`should load blogs from the content folder`, async () => {
|
||||
// GIVEN
|
||||
const blogPosts = await controller.getAllBlogPosts();
|
||||
|
||||
// WHEN
|
||||
const aKnownBlogPost = blogPosts.find((post) => post.title === 'Vibe Check #10');
|
||||
const aKnownBookReview = blogPosts.find((post) => post.title === 'After');
|
||||
const aMadeUpBlogPost = blogPosts.find((post) => post.title === 'Some made up blog post');
|
||||
|
||||
// then
|
||||
expect(aMadeUpBlogPost).toBeUndefined();
|
||||
expect(aKnownBlogPost).not.toBeUndefined();
|
||||
expect(aKnownBookReview).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should create a new file in the content folder`, async () => {
|
||||
// GIVEN
|
||||
const markdownContent = exampleMarkdown;
|
||||
describe(`getBlogPostBySlug`, () => {
|
||||
it(`should return null when the post doesn't exist`, async () => {
|
||||
// When
|
||||
const shouldBeNull = await controller.getBlogPostBySlug('some-made-up-blog-post');
|
||||
|
||||
// WHEN
|
||||
const blogPost = await controller.createBlogPost(
|
||||
fileName,
|
||||
markdownContent,
|
||||
);
|
||||
// Then
|
||||
expect(shouldBeNull).toBeNull();
|
||||
});
|
||||
|
||||
// THEN
|
||||
expect(blogPost).not.toBeNull();
|
||||
expect(blogPost.html).not.toBeNull();
|
||||
it(`should return the blog post when it exists`, async () => {
|
||||
// When
|
||||
const blogPost = await controller.getBlogPostBySlug('2023-02-03-vibe-check-10');
|
||||
|
||||
// Then
|
||||
expect(blogPost).not.toBeNull();
|
||||
expect(blogPost.title).toBe('Vibe Check #10');
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Finding content by slug`, () => {
|
||||
describe(`Finding a blog post`, () => {
|
||||
// GIVEN
|
||||
const slugForRealBlogPost = '2023-02-03-vibe-check-10';
|
||||
const slugForFakeBlogPost = 'some-made-up-blog-post';
|
||||
|
||||
it(`should return null if there's no blog post with the slug`, async () => {
|
||||
// WHEN
|
||||
const blogPost = await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost);
|
||||
|
||||
// THEN
|
||||
expect(blogPost).toBeNull();
|
||||
});
|
||||
|
||||
it(`should return the blog post if it exists`, async () => {
|
||||
// WHEN
|
||||
const blogPost = await controller.getAnyKindOfContentBySlug(slugForRealBlogPost);
|
||||
|
||||
// THEN
|
||||
expect(blogPost).not.toBeNull();
|
||||
expect(blogPost.title).toBe('Vibe Check #10');
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Finding a book review`, () => {
|
||||
const realSlug = 'after';
|
||||
const fakeSlug = 'some-made-up-book-review';
|
||||
|
||||
it(`should return null if there's no book review with the slug`, async () => {
|
||||
// WHEN
|
||||
const bookReview = await controller.getAnyKindOfContentBySlug(fakeSlug);
|
||||
|
||||
// THEN
|
||||
expect(bookReview).toBeNull();
|
||||
});
|
||||
|
||||
it(`should return the book review if it exists`, async () => {
|
||||
// WHEN
|
||||
const bookReview = await controller.getAnyKindOfContentBySlug(realSlug);
|
||||
|
||||
// THEN
|
||||
expect(bookReview).not.toBeNull();
|
||||
expect(bookReview.title).toBe('After');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Creating a new blog post as a file`, () => {
|
||||
const thisDirectory = import.meta.url
|
||||
.replace('file://', '')
|
||||
.split('/')
|
||||
.filter((part) => part !== 'BlogController.test.ts')
|
||||
.join('/');
|
||||
const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`;
|
||||
let controller: BlogController;
|
||||
|
||||
beforeEach(async () => {
|
||||
controller = await BlogController.singleton();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.markdownRepository.deleteBlogPostMarkdownFile(fileName);
|
||||
});
|
||||
|
||||
it(`should create a new file in the content folder`, async () => {
|
||||
// GIVEN
|
||||
const markdownContent = exampleMarkdown;
|
||||
|
||||
// WHEN
|
||||
const blogPost = await controller.createBlogPost(fileName, markdownContent);
|
||||
|
||||
// THEN
|
||||
expect(blogPost).not.toBeNull();
|
||||
expect(blogPost.html).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,158 +1,139 @@
|
|||
import type { BlogPost } from "./BlogPost.js";
|
||||
import type { BookReview } from "./BookReview.js";
|
||||
import { MarkdownRepository } from "./markdown-repository.js";
|
||||
import type { BlogPost } from './BlogPost.js';
|
||||
import type { BookReview } from './BookReview.js';
|
||||
import { MarkdownRepository } from './markdown-repository.js';
|
||||
|
||||
export interface BlogItem {
|
||||
title: string;
|
||||
date: string;
|
||||
content: string;
|
||||
slug: string;
|
||||
content_type: "blog" | "book_review" | "snout_street_studios";
|
||||
tags?: string[];
|
||||
title: string;
|
||||
date: string;
|
||||
content: string;
|
||||
slug: string;
|
||||
content_type: 'blog' | 'book_review' | 'snout_street_studios';
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface BlogPostListItem extends BlogItem {
|
||||
title: string;
|
||||
author: string;
|
||||
date: string;
|
||||
book_review: boolean;
|
||||
preview: string;
|
||||
tags: string[];
|
||||
title: string;
|
||||
author: string;
|
||||
date: string;
|
||||
book_review: boolean;
|
||||
preview: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface BookReviewListItem extends BlogItem {
|
||||
book_review: true;
|
||||
title: string;
|
||||
author: string;
|
||||
image: string;
|
||||
slug: string;
|
||||
score: number;
|
||||
finished: string;
|
||||
book_review: true;
|
||||
title: string;
|
||||
author: string;
|
||||
image: string;
|
||||
slug: string;
|
||||
score: number;
|
||||
finished: string;
|
||||
}
|
||||
|
||||
export class BlogController {
|
||||
private _markdownRepository: MarkdownRepository;
|
||||
private _markdownRepository: MarkdownRepository;
|
||||
|
||||
static async singleton(): Promise<BlogController> {
|
||||
const markdownRepository = await MarkdownRepository.singleton();
|
||||
return new BlogController(markdownRepository);
|
||||
}
|
||||
|
||||
constructor(markdownRepository: MarkdownRepository) {
|
||||
this._markdownRepository = markdownRepository;
|
||||
}
|
||||
|
||||
get markdownRepository(): MarkdownRepository {
|
||||
return this._markdownRepository;
|
||||
}
|
||||
|
||||
async createBlogPost(
|
||||
resolvedFileName: string,
|
||||
markdownContent: string,
|
||||
): Promise<BlogPost> {
|
||||
const createdBlogPost =
|
||||
await this._markdownRepository.createBlogPostMarkdownFile(
|
||||
resolvedFileName,
|
||||
markdownContent,
|
||||
);
|
||||
this._markdownRepository = await MarkdownRepository.singleton(true);
|
||||
return createdBlogPost;
|
||||
}
|
||||
|
||||
async getAllBlogPosts(
|
||||
pageSize?: number,
|
||||
): Promise<Array<BlogPostListItem | BookReviewListItem>> {
|
||||
const blogPosts = this._markdownRepository.blogPosts;
|
||||
|
||||
const bookReviews = this._markdownRepository.bookReviews;
|
||||
|
||||
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map(
|
||||
(blogPost) => {
|
||||
return this.blogPostToBlogPostListItem(blogPost);
|
||||
},
|
||||
);
|
||||
|
||||
const bookReviewListItems: BookReviewListItem[] =
|
||||
bookReviews.bookReviews.map((bookReview) => {
|
||||
return this.bookReviewToBookReviewListItem(bookReview);
|
||||
});
|
||||
|
||||
const allBlogPosts = [...blogPostListItems, ...bookReviewListItems].sort(
|
||||
(a, b) => (a.date > b.date ? -1 : 1),
|
||||
);
|
||||
|
||||
if (pageSize === undefined) {
|
||||
return allBlogPosts;
|
||||
static async singleton(): Promise<BlogController> {
|
||||
const markdownRepository = await MarkdownRepository.singleton();
|
||||
return new BlogController(markdownRepository);
|
||||
}
|
||||
|
||||
return allBlogPosts.slice(0, pageSize);
|
||||
}
|
||||
|
||||
async getBlogPostBySlug(slug: string): Promise<BlogPostListItem | null> {
|
||||
const blogPost = this._markdownRepository.getBlogPostBySlug(slug);
|
||||
if (blogPost) {
|
||||
return this.blogPostToBlogPostListItem(blogPost);
|
||||
constructor(markdownRepository: MarkdownRepository) {
|
||||
this._markdownRepository = markdownRepository;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getBlogPostsByTags(tags: string[]): Promise<BlogPostListItem[]> {
|
||||
const posts = await this.getAllBlogPosts();
|
||||
const blogPosts = posts.filter(
|
||||
(post) => post.content_type === "blog",
|
||||
) as BlogPostListItem[];
|
||||
return blogPosts
|
||||
.filter((post: BlogPostListItem) => post["tags"]?.length > 0)
|
||||
.filter((post: BlogPostListItem) =>
|
||||
(post.tags as string[]).some((tag) => tags.includes(tag)),
|
||||
);
|
||||
}
|
||||
|
||||
async getAnyKindOfContentBySlug(
|
||||
slug: string,
|
||||
): Promise<BookReviewListItem | BlogPostListItem | null> {
|
||||
const blogPost = this._markdownRepository.getBlogPostBySlug(slug);
|
||||
if (blogPost) {
|
||||
return this.blogPostToBlogPostListItem(blogPost);
|
||||
get markdownRepository(): MarkdownRepository {
|
||||
return this._markdownRepository;
|
||||
}
|
||||
|
||||
const bookReview = this._markdownRepository.getBookReviewBySlug(slug);
|
||||
if (bookReview) {
|
||||
return this.bookReviewToBookReviewListItem(bookReview);
|
||||
async createBlogPost(resolvedFileName: string, markdownContent: string): Promise<BlogPost> {
|
||||
const createdBlogPost = await this._markdownRepository.createBlogPostMarkdownFile(
|
||||
resolvedFileName,
|
||||
markdownContent
|
||||
);
|
||||
this._markdownRepository = await MarkdownRepository.singleton(true);
|
||||
return createdBlogPost;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
async getAllBlogPosts(pageSize?: number): Promise<Array<BlogPostListItem | BookReviewListItem>> {
|
||||
const blogPosts = this._markdownRepository.blogPosts;
|
||||
|
||||
private bookReviewToBookReviewListItem(
|
||||
bookReview: BookReview,
|
||||
): BookReviewListItem {
|
||||
return {
|
||||
book_review: true,
|
||||
title: bookReview.title,
|
||||
author: bookReview.author,
|
||||
date: bookReview.date.toISOString(),
|
||||
finished: bookReview.finished.toISOString(),
|
||||
image: bookReview.image,
|
||||
score: bookReview.score,
|
||||
slug: bookReview.slug,
|
||||
content: bookReview.html,
|
||||
content_type: "book_review",
|
||||
};
|
||||
}
|
||||
const bookReviews = this._markdownRepository.bookReviews;
|
||||
|
||||
private blogPostToBlogPostListItem(blogPost: BlogPost): BlogPostListItem {
|
||||
return {
|
||||
title: blogPost.title,
|
||||
author: blogPost.author,
|
||||
book_review: false,
|
||||
content: blogPost.html,
|
||||
date: blogPost.date.toISOString(),
|
||||
preview: blogPost.excerpt,
|
||||
slug: blogPost.slug,
|
||||
content_type: "blog",
|
||||
tags: blogPost.tags,
|
||||
};
|
||||
}
|
||||
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => {
|
||||
return this.blogPostToBlogPostListItem(blogPost);
|
||||
});
|
||||
|
||||
const bookReviewListItems: BookReviewListItem[] = bookReviews.bookReviews.map((bookReview) => {
|
||||
return this.bookReviewToBookReviewListItem(bookReview);
|
||||
});
|
||||
|
||||
const allBlogPosts = [...blogPostListItems, ...bookReviewListItems].sort((a, b) => (a.date > b.date ? -1 : 1));
|
||||
|
||||
if (pageSize === undefined) {
|
||||
return allBlogPosts;
|
||||
}
|
||||
|
||||
return allBlogPosts.slice(0, pageSize);
|
||||
}
|
||||
|
||||
async getBlogPostBySlug(slug: string): Promise<BlogPostListItem | null> {
|
||||
const blogPost = this._markdownRepository.getBlogPostBySlug(slug);
|
||||
if (blogPost) {
|
||||
return this.blogPostToBlogPostListItem(blogPost);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getBlogPostsByTags(tags: string[]): Promise<BlogPostListItem[]> {
|
||||
const posts = await this.getAllBlogPosts();
|
||||
const blogPosts = posts.filter((post) => post.content_type === 'blog') as BlogPostListItem[];
|
||||
return blogPosts
|
||||
.filter((post: BlogPostListItem) => post['tags']?.length > 0)
|
||||
.filter((post: BlogPostListItem) => (post.tags as string[]).some((tag) => tags.includes(tag)));
|
||||
}
|
||||
|
||||
async getAnyKindOfContentBySlug(slug: string): Promise<BookReviewListItem | BlogPostListItem | null> {
|
||||
const blogPost = this._markdownRepository.getBlogPostBySlug(slug);
|
||||
if (blogPost) {
|
||||
return this.blogPostToBlogPostListItem(blogPost);
|
||||
}
|
||||
|
||||
const bookReview = this._markdownRepository.getBookReviewBySlug(slug);
|
||||
if (bookReview) {
|
||||
return this.bookReviewToBookReviewListItem(bookReview);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bookReviewToBookReviewListItem(bookReview: BookReview): BookReviewListItem {
|
||||
return {
|
||||
book_review: true,
|
||||
title: bookReview.title,
|
||||
author: bookReview.author,
|
||||
date: bookReview.date.toISOString(),
|
||||
finished: bookReview.finished.toISOString(),
|
||||
image: bookReview.image,
|
||||
score: bookReview.score,
|
||||
slug: bookReview.slug,
|
||||
content: bookReview.html,
|
||||
content_type: 'book_review',
|
||||
};
|
||||
}
|
||||
|
||||
private blogPostToBlogPostListItem(blogPost: BlogPost): BlogPostListItem {
|
||||
return {
|
||||
title: blogPost.title,
|
||||
author: blogPost.author,
|
||||
book_review: false,
|
||||
content: blogPost.html,
|
||||
date: blogPost.date.toISOString(),
|
||||
preview: blogPost.excerpt,
|
||||
slug: blogPost.slug,
|
||||
content_type: 'blog',
|
||||
tags: blogPost.tags,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import type { BookReview } from './BookReview.js';
|
|||
export class RssFeed {
|
||||
private feed: Feed;
|
||||
|
||||
constructor(private readonly blogPosts: BlogPostSet, private readonly bookReviews: BookReviewSet) {
|
||||
constructor(
|
||||
private readonly blogPosts: BlogPostSet,
|
||||
private readonly bookReviews: BookReviewSet
|
||||
) {
|
||||
this.feed = new Feed({
|
||||
copyright: `All Rights Reserved Thomas Wilson 2023`,
|
||||
id: 'https://www.thomaswilson.xyz',
|
||||
|
|
|
|||
|
|
@ -1,45 +1,41 @@
|
|||
import type { Cookies } from "@sveltejs/kit";
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
|
||||
export class CookieAuthentication {
|
||||
private readonly cookieValue: string;
|
||||
private readonly cookieValueArray: string[];
|
||||
public static cookieName = "auth";
|
||||
public static adminAuthRole = "admin";
|
||||
private readonly cookieValue: string;
|
||||
private readonly cookieValueArray: string[];
|
||||
public static cookieName = 'auth';
|
||||
public static adminAuthRole = 'admin';
|
||||
|
||||
constructor(private readonly cookies: Cookies) {
|
||||
this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? "";
|
||||
this.cookieValueArray = this.cookieValue.split(",");
|
||||
}
|
||||
|
||||
public get isAuthdAsAdmin(): boolean {
|
||||
let isAuthdAsAdmin = false;
|
||||
|
||||
if (this.cookieValueArray.includes(CookieAuthentication.adminAuthRole)) {
|
||||
isAuthdAsAdmin = true;
|
||||
constructor(private readonly cookies: Cookies) {
|
||||
this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? '';
|
||||
this.cookieValueArray = this.cookieValue.split(',');
|
||||
}
|
||||
|
||||
return isAuthdAsAdmin;
|
||||
}
|
||||
public get isAuthdAsAdmin(): boolean {
|
||||
let isAuthdAsAdmin = false;
|
||||
|
||||
public logout() {
|
||||
if (!this.isAuthdAsAdmin) return;
|
||||
if (this.cookieValueArray.includes(CookieAuthentication.adminAuthRole)) {
|
||||
isAuthdAsAdmin = true;
|
||||
}
|
||||
|
||||
this.cookies.delete(CookieAuthentication.cookieName, { path: "/" });
|
||||
}
|
||||
|
||||
public setAdminAuthentication(isAuthd: boolean) {
|
||||
let value = this.cookieValue;
|
||||
|
||||
if (isAuthd) {
|
||||
value = Array.from(
|
||||
new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole]),
|
||||
).join(",");
|
||||
} else {
|
||||
value = this.cookieValueArray
|
||||
.filter((i) => i !== CookieAuthentication.adminAuthRole)
|
||||
.join(",");
|
||||
return isAuthdAsAdmin;
|
||||
}
|
||||
|
||||
this.cookies.set(CookieAuthentication.cookieName, value, { path: "/" });
|
||||
}
|
||||
public logout() {
|
||||
if (!this.isAuthdAsAdmin) return;
|
||||
|
||||
this.cookies.delete(CookieAuthentication.cookieName, { path: '/' });
|
||||
}
|
||||
|
||||
public setAdminAuthentication(isAuthd: boolean) {
|
||||
let value = this.cookieValue;
|
||||
|
||||
if (isAuthd) {
|
||||
value = Array.from(new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole])).join(',');
|
||||
} else {
|
||||
value = this.cookieValueArray.filter((i) => i !== CookieAuthentication.adminAuthRole).join(',');
|
||||
}
|
||||
|
||||
this.cookies.set(CookieAuthentication.cookieName, value, { path: '/' });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,103 +1,94 @@
|
|||
import { describe, it, expect, beforeAll, beforeEach } from "vitest";
|
||||
import { MarkdownRepository } from "./markdown-repository.js";
|
||||
import { resolve, dirname } from "path";
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { MarkdownRepository } from './markdown-repository.js';
|
||||
import { resolve, dirname } from 'path';
|
||||
|
||||
import { aBlogPost } from "./test-builders/blog-post-builder.js";
|
||||
import { aBlogPost } from './test-builders/blog-post-builder.js';
|
||||
|
||||
const blogPostImport = import.meta.glob(`./test-fixtures/blog-*.md`, {
|
||||
as: "raw",
|
||||
as: 'raw',
|
||||
});
|
||||
const bookReviewImport = import.meta.glob(`./test-fixtures/book-review-*.md`, {
|
||||
as: "raw",
|
||||
as: 'raw',
|
||||
});
|
||||
const snoutStreetPostImport = import.meta.glob(
|
||||
`./test-fixtures/snout-street-studio-*.md`,
|
||||
{ as: "raw" },
|
||||
);
|
||||
const snoutStreetPostImport = import.meta.glob(`./test-fixtures/snout-street-studio-*.md`, { as: 'raw' });
|
||||
|
||||
const expectedHtml = `<p>This is a blog post written in markdown.</p>
|
||||
<p>This is a <a href="http://www.bbc.co.uk">link</a></p>`;
|
||||
|
||||
describe(`Blog MarkdownRepository`, () => {
|
||||
let repository: MarkdownRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
repository = await MarkdownRepository.fromViteGlobImport(
|
||||
blogPostImport,
|
||||
bookReviewImport,
|
||||
);
|
||||
});
|
||||
|
||||
it(`should load`, async () => {
|
||||
// GIVEN
|
||||
const expectedBlogPost = aBlogPost()
|
||||
.withAuthor("Thomas Wilson")
|
||||
.withDate(new Date("2023-02-01T08:00:00Z"))
|
||||
.withSlug("2023-02-01-test")
|
||||
.withTitle("Test Blog Post")
|
||||
.withExcerpt("This is a blog post written in markdown. This is a link")
|
||||
.withHtml(expectedHtml)
|
||||
.withFileName("blog-2023-02-01-test.md")
|
||||
.build();
|
||||
|
||||
// WHEN
|
||||
const blogPost =
|
||||
repository.blogPosts.getBlogPostWithTitle("Test Blog Post");
|
||||
|
||||
// THEN
|
||||
expect(repository).toBeDefined();
|
||||
expect(blogPost).toStrictEqual(expectedBlogPost);
|
||||
});
|
||||
|
||||
it(`should automatically build all the blog posts and book reviews`, async () => {
|
||||
// WHEN/THEN
|
||||
expect(repository.blogPosts.blogPosts[0].html).not.toBeNull();
|
||||
expect(repository.bookReviews.bookReviews[0].html).not.toBeNull();
|
||||
});
|
||||
|
||||
describe(`Finding by Slug`, () => {
|
||||
it(`should return null if there's neither a blog post nor a book review with the slug`, async () => {
|
||||
// WHEN
|
||||
const markdownFile = repository.getBlogPostBySlug("non-existent-slug");
|
||||
|
||||
// THEN
|
||||
expect(markdownFile).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Deleting markdown files`, () => {
|
||||
let repository: MarkdownRepository;
|
||||
const currentDirectory = dirname(import.meta.url.replace("file://", ""));
|
||||
|
||||
beforeAll(async () => {
|
||||
repository = await MarkdownRepository.fromViteGlobImport(
|
||||
blogPostImport,
|
||||
bookReviewImport,
|
||||
snoutStreetPostImport,
|
||||
);
|
||||
|
||||
const resolvedPath = resolve(
|
||||
`${currentDirectory}/test-fixtures/test-file.md`,
|
||||
);
|
||||
await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml);
|
||||
beforeEach(async () => {
|
||||
repository = await MarkdownRepository.fromViteGlobImport(blogPostImport, bookReviewImport);
|
||||
});
|
||||
|
||||
it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => {
|
||||
// GIVEN
|
||||
const theFileName = "non-existent-file.md";
|
||||
it(`should load`, async () => {
|
||||
// GIVEN
|
||||
const expectedBlogPost = aBlogPost()
|
||||
.withAuthor('Thomas Wilson')
|
||||
.withDate(new Date('2023-02-01T08:00:00Z'))
|
||||
.withSlug('2023-02-01-test')
|
||||
.withTitle('Test Blog Post')
|
||||
.withExcerpt('This is a blog post written in markdown. This is a link')
|
||||
.withHtml(expectedHtml)
|
||||
.withFileName('blog-2023-02-01-test.md')
|
||||
.build();
|
||||
|
||||
// WHEN/THEN
|
||||
expect(async () =>
|
||||
repository.deleteBlogPostMarkdownFile(theFileName),
|
||||
).rejects.toThrowError(`File 'non-existent-file.md' not found.`);
|
||||
// WHEN
|
||||
const blogPost = repository.blogPosts.getBlogPostWithTitle('Test Blog Post');
|
||||
|
||||
// THEN
|
||||
expect(repository).toBeDefined();
|
||||
expect(blogPost).toStrictEqual(expectedBlogPost);
|
||||
});
|
||||
|
||||
it(`should successfully delete a file when it does exist`, async () => {
|
||||
// GIVEN
|
||||
const fileName = `${currentDirectory}/test-fixtures/test-file.md`;
|
||||
|
||||
// WHEN
|
||||
await repository.deleteBlogPostMarkdownFile(fileName);
|
||||
it(`should automatically build all the blog posts and book reviews`, async () => {
|
||||
// WHEN/THEN
|
||||
expect(repository.blogPosts.blogPosts[0].html).not.toBeNull();
|
||||
expect(repository.bookReviews.bookReviews[0].html).not.toBeNull();
|
||||
});
|
||||
|
||||
describe(`Finding by Slug`, () => {
|
||||
it(`should return null if there's neither a blog post nor a book review with the slug`, async () => {
|
||||
// WHEN
|
||||
const markdownFile = repository.getBlogPostBySlug('non-existent-slug');
|
||||
|
||||
// THEN
|
||||
expect(markdownFile).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Deleting markdown files`, () => {
|
||||
let repository: MarkdownRepository;
|
||||
const currentDirectory = dirname(import.meta.url.replace('file://', ''));
|
||||
|
||||
beforeAll(async () => {
|
||||
repository = await MarkdownRepository.fromViteGlobImport(
|
||||
blogPostImport,
|
||||
bookReviewImport,
|
||||
snoutStreetPostImport
|
||||
);
|
||||
|
||||
const resolvedPath = resolve(`${currentDirectory}/test-fixtures/test-file.md`);
|
||||
await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml);
|
||||
});
|
||||
|
||||
it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => {
|
||||
// GIVEN
|
||||
const theFileName = 'non-existent-file.md';
|
||||
|
||||
// WHEN/THEN
|
||||
expect(async () => repository.deleteBlogPostMarkdownFile(theFileName)).rejects.toThrowError(
|
||||
`File 'non-existent-file.md' not found.`
|
||||
);
|
||||
});
|
||||
|
||||
it(`should successfully delete a file when it does exist`, async () => {
|
||||
// GIVEN
|
||||
const fileName = `${currentDirectory}/test-fixtures/test-file.md`;
|
||||
|
||||
// WHEN
|
||||
await repository.deleteBlogPostMarkdownFile(fileName);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,222 +1,186 @@
|
|||
import { writeFile, unlink, existsSync } from "fs";
|
||||
import { writeFile, unlink, existsSync } from 'fs';
|
||||
|
||||
import { BlogPost } from "./BlogPost.js";
|
||||
import { MarkdownFile } from "./MarkdownFile.js";
|
||||
import { BlogPostSet } from "./BlogPostSet.js";
|
||||
import { BookReviewSet } from "./BookReviewSet.js";
|
||||
import { BookReview } from "./BookReview.js";
|
||||
import { BlogPost } from './BlogPost.js';
|
||||
import { MarkdownFile } from './MarkdownFile.js';
|
||||
import { BlogPostSet } from './BlogPostSet.js';
|
||||
import { BookReviewSet } from './BookReviewSet.js';
|
||||
import { BookReview } from './BookReview.js';
|
||||
|
||||
// We have to duplicate the `../..` here because import.meta must have a static string,
|
||||
// and it (rightfully) cannot have dynamic locations
|
||||
const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, {
|
||||
as: "raw",
|
||||
as: 'raw',
|
||||
});
|
||||
const bookReviewsMetaGlobImport = import.meta.glob(
|
||||
`../../content/book-reviews/*.md`,
|
||||
{ as: "raw" },
|
||||
);
|
||||
const bookReviewsMetaGlobImport = import.meta.glob(`../../content/book-reviews/*.md`, { as: 'raw' });
|
||||
|
||||
interface BlogPostFrontmatterValues {
|
||||
title: string;
|
||||
slug: string;
|
||||
date: Date;
|
||||
author: string;
|
||||
tags?: string[];
|
||||
title: string;
|
||||
slug: string;
|
||||
date: Date;
|
||||
author: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
interface BookReviewFrontmatterValues {
|
||||
title: string;
|
||||
author: string; // Author of the book, not the review
|
||||
slug: string;
|
||||
date: Date;
|
||||
finished: Date;
|
||||
score: number;
|
||||
image: string;
|
||||
title: string;
|
||||
author: string; // Author of the book, not the review
|
||||
slug: string;
|
||||
date: Date;
|
||||
finished: Date;
|
||||
score: number;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export class MarkdownRepository {
|
||||
readonly blogPosts: BlogPostSet;
|
||||
readonly bookReviews: BookReviewSet;
|
||||
private static _singleton: MarkdownRepository;
|
||||
readonly blogPosts: BlogPostSet;
|
||||
readonly bookReviews: BookReviewSet;
|
||||
private static _singleton: MarkdownRepository;
|
||||
|
||||
private constructor(blogPosts: BlogPost[], bookReviews: BookReview[]) {
|
||||
this.blogPosts = new BlogPostSet(blogPosts);
|
||||
this.bookReviews = new BookReviewSet(bookReviews);
|
||||
}
|
||||
|
||||
public static async singleton(
|
||||
forceRefresh = false,
|
||||
): Promise<MarkdownRepository> {
|
||||
if (forceRefresh || !this._singleton) {
|
||||
console.log(
|
||||
`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`,
|
||||
);
|
||||
this._singleton = await MarkdownRepository.fromViteGlobImport(
|
||||
blogPostMetaGlobImport,
|
||||
bookReviewsMetaGlobImport,
|
||||
);
|
||||
private constructor(blogPosts: BlogPost[], bookReviews: BookReview[]) {
|
||||
this.blogPosts = new BlogPostSet(blogPosts);
|
||||
this.bookReviews = new BookReviewSet(bookReviews);
|
||||
}
|
||||
|
||||
return this._singleton;
|
||||
}
|
||||
|
||||
public static async fromViteGlobImport(
|
||||
blogGlobImport: any,
|
||||
bookReviewGlobImport: any,
|
||||
): Promise<MarkdownRepository> {
|
||||
let fileImports: MarkdownFile<BlogPostFrontmatterValues>[] = [];
|
||||
let blogPosts: BlogPost[] = [];
|
||||
let bookReviews: BookReview[] = [];
|
||||
|
||||
const blogPostFiles = Object.entries(blogGlobImport);
|
||||
|
||||
for (const blogPostFile of blogPostFiles) {
|
||||
const [filename, module] = blogPostFile as [
|
||||
string,
|
||||
() => Promise<string>,
|
||||
];
|
||||
try {
|
||||
const markdownFile =
|
||||
await MarkdownFile.build<BlogPostFrontmatterValues>(
|
||||
filename,
|
||||
await module(),
|
||||
);
|
||||
|
||||
const blogPost = new BlogPost({
|
||||
excerpt: markdownFile.excerpt,
|
||||
html: markdownFile.html,
|
||||
title: markdownFile.frontmatter.title,
|
||||
slug: markdownFile.frontmatter.slug,
|
||||
author: markdownFile.frontmatter.author,
|
||||
date: markdownFile.frontmatter.date,
|
||||
fileName: filename,
|
||||
tags: markdownFile.frontmatter.tags ?? [],
|
||||
});
|
||||
|
||||
fileImports = [...fileImports, markdownFile];
|
||||
blogPosts = [...blogPosts, blogPost];
|
||||
} catch (e) {
|
||||
console.error({
|
||||
message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const bookReviewFile of Object.entries(bookReviewGlobImport)) {
|
||||
const [filename, module] = bookReviewFile as [
|
||||
string,
|
||||
() => Promise<string>,
|
||||
];
|
||||
try {
|
||||
const markdownFile =
|
||||
await MarkdownFile.build<BookReviewFrontmatterValues>(
|
||||
filename,
|
||||
await module(),
|
||||
);
|
||||
|
||||
const bookReview = new BookReview({
|
||||
author: markdownFile.frontmatter.author,
|
||||
title: markdownFile.frontmatter.title,
|
||||
slug: markdownFile.frontmatter.slug,
|
||||
date: markdownFile.frontmatter.date,
|
||||
draft: false,
|
||||
finished: markdownFile.frontmatter.finished,
|
||||
image: markdownFile.frontmatter.image,
|
||||
score: markdownFile.frontmatter.score,
|
||||
html: markdownFile.html,
|
||||
});
|
||||
|
||||
bookReviews = [...bookReviews, bookReview];
|
||||
} catch (e) {
|
||||
console.error({
|
||||
message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`,
|
||||
);
|
||||
const repository = new MarkdownRepository(blogPosts, bookReviews);
|
||||
console.log(`[MarkdownRepository::fromViteGlobImport] Built all posts.`);
|
||||
return repository;
|
||||
}
|
||||
|
||||
getBlogPostBySlug(slug: string): BlogPost | null {
|
||||
return (
|
||||
this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
getBookReviewBySlug(slug: string): BookReview | null {
|
||||
return (
|
||||
this.bookReviews.bookReviews.find(
|
||||
(bookReview) => bookReview.slug === slug,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
async createBlogPostMarkdownFile(
|
||||
resolvedPath: string,
|
||||
contents: string,
|
||||
): Promise<BlogPost> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
writeFile(resolvedPath, contents, (err) => {
|
||||
if (err) {
|
||||
console.error({
|
||||
message: `createBlogPostMarkdownFile: Caught error while writing file ${resolvedPath}`,
|
||||
err,
|
||||
error: JSON.stringify(err),
|
||||
});
|
||||
reject(err);
|
||||
public static async singleton(forceRefresh = false): Promise<MarkdownRepository> {
|
||||
if (forceRefresh || !this._singleton) {
|
||||
console.log(`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`);
|
||||
this._singleton = await MarkdownRepository.fromViteGlobImport(
|
||||
blogPostMetaGlobImport,
|
||||
bookReviewsMetaGlobImport
|
||||
);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}).then(async () => {
|
||||
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(
|
||||
resolvedPath,
|
||||
contents,
|
||||
);
|
||||
|
||||
const blogPost = new BlogPost({
|
||||
html: markdownFile.html,
|
||||
excerpt: markdownFile.excerpt,
|
||||
title: markdownFile.frontmatter?.title ?? undefined,
|
||||
slug: markdownFile.frontmatter?.slug ?? undefined,
|
||||
author: markdownFile.frontmatter?.author ?? undefined,
|
||||
date: markdownFile.frontmatter?.date ?? undefined,
|
||||
fileName: resolvedPath,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
return blogPost;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBlogPostMarkdownFile(resolvedFilePath: string): Promise<void> {
|
||||
const isPresent = existsSync(resolvedFilePath);
|
||||
|
||||
if (!isPresent) {
|
||||
throw `Sausages File '${resolvedFilePath}' not found.`;
|
||||
return this._singleton;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
unlink(resolvedFilePath, (err) => {
|
||||
if (err) {
|
||||
console.error({
|
||||
message: `deleteBlogPostMarkdownFile: Caught error while deleting file ${resolvedFilePath}`,
|
||||
err,
|
||||
error: JSON.stringify(err),
|
||||
});
|
||||
reject(err);
|
||||
public static async fromViteGlobImport(
|
||||
blogGlobImport: any,
|
||||
bookReviewGlobImport: any
|
||||
): Promise<MarkdownRepository> {
|
||||
let fileImports: MarkdownFile<BlogPostFrontmatterValues>[] = [];
|
||||
let blogPosts: BlogPost[] = [];
|
||||
let bookReviews: BookReview[] = [];
|
||||
|
||||
const blogPostFiles = Object.entries(blogGlobImport);
|
||||
|
||||
for (const blogPostFile of blogPostFiles) {
|
||||
const [filename, module] = blogPostFile as [string, () => Promise<string>];
|
||||
try {
|
||||
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(filename, await module());
|
||||
|
||||
const blogPost = new BlogPost({
|
||||
excerpt: markdownFile.excerpt,
|
||||
html: markdownFile.html,
|
||||
title: markdownFile.frontmatter.title,
|
||||
slug: markdownFile.frontmatter.slug,
|
||||
author: markdownFile.frontmatter.author,
|
||||
date: markdownFile.frontmatter.date,
|
||||
fileName: filename,
|
||||
tags: markdownFile.frontmatter.tags ?? [],
|
||||
});
|
||||
|
||||
fileImports = [...fileImports, markdownFile];
|
||||
blogPosts = [...blogPosts, blogPost];
|
||||
} catch (e) {
|
||||
console.error({
|
||||
message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
for (const bookReviewFile of Object.entries(bookReviewGlobImport)) {
|
||||
const [filename, module] = bookReviewFile as [string, () => Promise<string>];
|
||||
try {
|
||||
const markdownFile = await MarkdownFile.build<BookReviewFrontmatterValues>(filename, await module());
|
||||
|
||||
const bookReview = new BookReview({
|
||||
author: markdownFile.frontmatter.author,
|
||||
title: markdownFile.frontmatter.title,
|
||||
slug: markdownFile.frontmatter.slug,
|
||||
date: markdownFile.frontmatter.date,
|
||||
draft: false,
|
||||
finished: markdownFile.frontmatter.finished,
|
||||
image: markdownFile.frontmatter.image,
|
||||
score: markdownFile.frontmatter.score,
|
||||
html: markdownFile.html,
|
||||
});
|
||||
|
||||
bookReviews = [...bookReviews, bookReview];
|
||||
} catch (e) {
|
||||
console.error({
|
||||
message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`);
|
||||
const repository = new MarkdownRepository(blogPosts, bookReviews);
|
||||
console.log(`[MarkdownRepository::fromViteGlobImport] Built all posts.`);
|
||||
return repository;
|
||||
}
|
||||
|
||||
getBlogPostBySlug(slug: string): BlogPost | null {
|
||||
return this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ?? null;
|
||||
}
|
||||
|
||||
getBookReviewBySlug(slug: string): BookReview | null {
|
||||
return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null;
|
||||
}
|
||||
|
||||
async createBlogPostMarkdownFile(resolvedPath: string, contents: string): Promise<BlogPost> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
writeFile(resolvedPath, contents, (err) => {
|
||||
if (err) {
|
||||
console.error({
|
||||
message: `createBlogPostMarkdownFile: Caught error while writing file ${resolvedPath}`,
|
||||
err,
|
||||
error: JSON.stringify(err),
|
||||
});
|
||||
reject(err);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}).then(async () => {
|
||||
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(resolvedPath, contents);
|
||||
|
||||
const blogPost = new BlogPost({
|
||||
html: markdownFile.html,
|
||||
excerpt: markdownFile.excerpt,
|
||||
title: markdownFile.frontmatter?.title ?? undefined,
|
||||
slug: markdownFile.frontmatter?.slug ?? undefined,
|
||||
author: markdownFile.frontmatter?.author ?? undefined,
|
||||
date: markdownFile.frontmatter?.date ?? undefined,
|
||||
fileName: resolvedPath,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
return blogPost;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBlogPostMarkdownFile(resolvedFilePath: string): Promise<void> {
|
||||
const isPresent = existsSync(resolvedFilePath);
|
||||
|
||||
if (!isPresent) {
|
||||
throw `Sausages File '${resolvedFilePath}' not found.`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
unlink(resolvedFilePath, (err) => {
|
||||
if (err) {
|
||||
console.error({
|
||||
message: `deleteBlogPostMarkdownFile: Caught error while deleting file ${resolvedFilePath}`,
|
||||
err,
|
||||
error: JSON.stringify(err),
|
||||
});
|
||||
reject(err);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
import { SimplePasswordAuthenticator } from './simple-password-authenticator';
|
||||
|
||||
import { it, expect} from 'vitest'
|
||||
import { it, expect } from 'vitest';
|
||||
|
||||
it('should do nothing when things are valid', () => {
|
||||
// GIVEN
|
||||
const authenticator = new SimplePasswordAuthenticator('expected-password');
|
||||
// GIVEN
|
||||
const authenticator = new SimplePasswordAuthenticator('expected-password');
|
||||
|
||||
// WHEN
|
||||
const result = authenticator.authenticate('expected-password');
|
||||
// WHEN
|
||||
const result = authenticator.authenticate('expected-password');
|
||||
|
||||
//
|
||||
expect(result).toBeTruthy();
|
||||
})
|
||||
//
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not authenticate when the password is invalid', () => {
|
||||
// GIVEN
|
||||
const authenticator = new SimplePasswordAuthenticator('expected-password');
|
||||
// GIVEN
|
||||
const authenticator = new SimplePasswordAuthenticator('expected-password');
|
||||
|
||||
// WHEN
|
||||
const result = authenticator.authenticate('invalid-password');
|
||||
// WHEN
|
||||
const result = authenticator.authenticate('invalid-password');
|
||||
|
||||
// THEN
|
||||
expect(result).toBeFalsy();
|
||||
|
||||
})
|
||||
// THEN
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import type { Authenticator } from './Authenticator';
|
||||
|
||||
export class SimplePasswordAuthenticator implements Authenticator{
|
||||
constructor(private readonly password: string) {
|
||||
if (this.password === undefined) {
|
||||
throw new Error('Password must be defined');
|
||||
}
|
||||
}
|
||||
export class SimplePasswordAuthenticator implements Authenticator {
|
||||
constructor(private readonly password: string) {
|
||||
if (this.password === undefined) {
|
||||
throw new Error('Password must be defined');
|
||||
}
|
||||
}
|
||||
|
||||
authenticate(password: string): boolean {
|
||||
return this.password === password;
|
||||
}
|
||||
}
|
||||
authenticate(password: string): boolean {
|
||||
return this.password === password;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
export interface SunriseOrSunsetPhotoSet {
|
||||
total: number
|
||||
total_pages: number
|
||||
search_term: string
|
||||
results: SunriseOrSunsetPhoto[]
|
||||
total: number;
|
||||
total_pages: number;
|
||||
search_term: string;
|
||||
results: SunriseOrSunsetPhoto[];
|
||||
}
|
||||
|
||||
export interface SunriseOrSunsetPhoto {
|
||||
id: string
|
||||
description: string
|
||||
username: string
|
||||
username_url: string
|
||||
small_url: string
|
||||
id: string;
|
||||
description: string;
|
||||
username: string;
|
||||
username_url: string;
|
||||
small_url: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { notStrictEqual } from "node:assert";
|
||||
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
|
||||
import { PrismaClient } from "../../generated/prisma/client.js";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import { notStrictEqual } from 'node:assert';
|
||||
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
|
||||
import { PrismaClient } from '../../generated/prisma/client.js';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export class PrismaClientFactory {
|
||||
private constructor(private readonly databaseUrl: string) {}
|
||||
public static fromEnv(): PrismaClientFactory {
|
||||
const value = env.PRIVATE_DATABASE_URL ?? "";
|
||||
notStrictEqual(value, "", `"env.PRIVATE_DATABASE_URL" must be defined`);
|
||||
private constructor(private readonly databaseUrl: string) {}
|
||||
public static fromEnv(): PrismaClientFactory {
|
||||
const value = env.PRIVATE_DATABASE_URL ?? '';
|
||||
notStrictEqual(value, '', `"env.PRIVATE_DATABASE_URL" must be defined`);
|
||||
|
||||
return new PrismaClientFactory(value);
|
||||
}
|
||||
return new PrismaClientFactory(value);
|
||||
}
|
||||
|
||||
createClient(): PrismaClient {
|
||||
const adapter = new PrismaBetterSqlite3({ url: this.databaseUrl });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
return prisma;
|
||||
}
|
||||
createClient(): PrismaClient {
|
||||
const adapter = new PrismaBetterSqlite3({ url: this.databaseUrl });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
return prisma;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { redirect } from "@sveltejs/kit";
|
||||
import type { LayoutServerLoad } from "./$types.js";
|
||||
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { LayoutServerLoad } from './$types.js';
|
||||
import { CookieAuthentication } from '$lib/blog/auth/CookieAuthentication.js';
|
||||
|
||||
export const load: LayoutServerLoad = ({ cookies, route }) => {
|
||||
const auth = new CookieAuthentication(cookies)
|
||||
const isAuthd = auth.isAuthdAsAdmin
|
||||
const auth = new CookieAuthentication(cookies);
|
||||
const isAuthd = auth.isAuthdAsAdmin;
|
||||
|
||||
if (route.id === '/admin/login' && isAuthd) {
|
||||
return redirect(307, '/admin')
|
||||
} else if (!isAuthd && route.id !== '/admin/login') {
|
||||
return redirect(307, '/admin/login')
|
||||
}
|
||||
if (route.id === '/admin/login' && isAuthd) {
|
||||
return redirect(307, '/admin');
|
||||
} else if (!isAuthd && route.id !== '/admin/login') {
|
||||
return redirect(307, '/admin/login');
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
import { PRIVATE_ADMIN_AUTH_TOKEN } from "$env/static/private";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { Actions} from "./$types.js";
|
||||
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
|
||||
import { PRIVATE_ADMIN_AUTH_TOKEN } from '$env/static/private';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types.js';
|
||||
import { CookieAuthentication } from '$lib/blog/auth/CookieAuthentication.js';
|
||||
|
||||
export const actions = {
|
||||
default: async ({cookies, request}) => {
|
||||
const formData = await request.formData()
|
||||
const token = formData.get('token')
|
||||
default: async ({ cookies, request }) => {
|
||||
const formData = await request.formData();
|
||||
const token = formData.get('token');
|
||||
|
||||
const isAuthd = PRIVATE_ADMIN_AUTH_TOKEN === token;
|
||||
const auth = new CookieAuthentication(cookies)
|
||||
const isAuthd = PRIVATE_ADMIN_AUTH_TOKEN === token;
|
||||
const auth = new CookieAuthentication(cookies);
|
||||
|
||||
auth.setAdminAuthentication(isAuthd)
|
||||
auth.setAdminAuthentication(isAuthd);
|
||||
|
||||
if (isAuthd) {
|
||||
return redirect(307, '/admin')
|
||||
}
|
||||
if (isAuthd) {
|
||||
return redirect(307, '/admin');
|
||||
}
|
||||
|
||||
return {
|
||||
isAuthd
|
||||
}
|
||||
}
|
||||
|
||||
} satisfies Actions
|
||||
return {
|
||||
isAuthd,
|
||||
};
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
|
||||
import { redirect, type ServerLoad } from "@sveltejs/kit";
|
||||
import { CookieAuthentication } from '$lib/blog/auth/CookieAuthentication.js';
|
||||
import { redirect, type ServerLoad } from '@sveltejs/kit';
|
||||
|
||||
export const GET: ServerLoad = ({ cookies }) => {
|
||||
new CookieAuthentication(cookies).logout();
|
||||
redirect(307, "/");
|
||||
new CookieAuthentication(cookies).logout();
|
||||
redirect(307, '/');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,47 +1,41 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { Buffer } from "node:buffer";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
|
||||
import type { Actions, ServerLoad } from "@sveltejs/kit";
|
||||
import { randomUUID } from "crypto";
|
||||
import { PRIVATE_PHOTO_UPLOAD_DIR } from '$env/static/private';
|
||||
import type { Actions, ServerLoad } from '@sveltejs/kit';
|
||||
import { LocalFileRepository } from '$lib/LocalFileRepository.js';
|
||||
|
||||
export const load: ServerLoad = async ({ locals }) => {
|
||||
const photos = await locals.prisma.photoPost.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
fileName: true,
|
||||
title: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
return { photos };
|
||||
const photos = await locals.prisma.photoPost.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
filePath: true,
|
||||
fileName: true,
|
||||
title: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
return { photos };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, locals }) => {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File;
|
||||
const title = formData.get("title") as string;
|
||||
const description = formData.get("description") as string;
|
||||
default: async ({ request, locals }) => {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
const title = formData.get('title') as string;
|
||||
const description = formData.get('description') as string;
|
||||
|
||||
const filetype = file.type.split("/")[1];
|
||||
const fileName = `${randomUUID()}.${filetype}`;
|
||||
const fileLocation = join(PRIVATE_PHOTO_UPLOAD_DIR, fileName);
|
||||
const fileRepo = new LocalFileRepository(PRIVATE_PHOTO_UPLOAD_DIR);
|
||||
const { fileName, filePath } = await fileRepo.saveFile(file);
|
||||
|
||||
const fileContentBuffer = await file.arrayBuffer();
|
||||
await writeFile(fileLocation, Buffer.from(fileContentBuffer));
|
||||
await locals.prisma.photoPost.create({
|
||||
data: {
|
||||
fileName,
|
||||
filePath,
|
||||
title,
|
||||
description,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await locals.prisma.photoPost.create({
|
||||
data: {
|
||||
fileName,
|
||||
title,
|
||||
description,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
return { success: true };
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { PhotoPost } from "../../../../generated/prisma/client.js";
|
||||
import FeedItem from "./FeedItem.svelte";
|
||||
|
||||
const { photos }: { photos: PhotoPost[] } = $props();
|
||||
</script>
|
||||
|
||||
<ul class="photo-feed">
|
||||
{#each photos as photo, id}
|
||||
<li class="item">
|
||||
<img width="250" src={`/image/${photo.fileName}`} alt={photo.title} />
|
||||
<p>{photo.title}</p>
|
||||
</li>
|
||||
<FeedItem {photo} />
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
|
|
@ -21,15 +19,4 @@
|
|||
list-style: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item img {
|
||||
width: 250px;
|
||||
height: auto;
|
||||
height: fit-content;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
9
src/routes/admin/photos/[id]/+error.svelte
Normal file
9
src/routes/admin/photos/[id]/+error.svelte
Normal 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>
|
||||
77
src/routes/admin/photos/[id]/+page.server.ts
Normal file
77
src/routes/admin/photos/[id]/+page.server.ts
Normal 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;
|
||||
19
src/routes/admin/photos/[id]/+page.svelte
Normal file
19
src/routes/admin/photos/[id]/+page.svelte
Normal 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>
|
||||
131
src/routes/admin/photos/[id]/EditPhotoForm.svelte
Normal file
131
src/routes/admin/photos/[id]/EditPhotoForm.svelte
Normal 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>
|
||||
7
src/routes/admin/photos/upload/[id]/+page.server.ts
Normal file
7
src/routes/admin/photos/upload/[id]/+page.server.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { PageServerLoad } from '../upload/[id]/$types.js';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const { id } = params;
|
||||
|
||||
return { id };
|
||||
};
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { SunriseSunsetController } from './SunriseSunsetController.js'
|
||||
import { SunriseSunsetController } from './SunriseSunsetController.js';
|
||||
|
||||
const controller = new SunriseSunsetController()
|
||||
const controller = new SunriseSunsetController();
|
||||
|
||||
export const GET = async () => {
|
||||
const now = new Date()
|
||||
const body = controller.getSunriseSunsetPhotoForDate(now)
|
||||
const now = new Date();
|
||||
const body = controller.getSunriseSunsetPhotoForDate(now);
|
||||
|
||||
const response = {
|
||||
status: 200,
|
||||
body,
|
||||
}
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(response.body))
|
||||
}
|
||||
return new Response(JSON.stringify(response.body));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
import { it, describe, expect, beforeAll } from 'vitest'
|
||||
import {
|
||||
type ISunriseSunsetController,
|
||||
SunriseSunsetController,
|
||||
} from './SunriseSunsetController'
|
||||
import { it, describe, expect, beforeAll } from 'vitest';
|
||||
import { type ISunriseSunsetController, SunriseSunsetController } from './SunriseSunsetController';
|
||||
|
||||
describe('SunriseSunsetController', () => {
|
||||
let controller: ISunriseSunsetController
|
||||
let controller: ISunriseSunsetController;
|
||||
|
||||
beforeAll(() => {
|
||||
controller = new SunriseSunsetController()
|
||||
})
|
||||
controller = new SunriseSunsetController();
|
||||
});
|
||||
|
||||
it(`Should return a known photo for a known date`, () => {
|
||||
// GIVEN
|
||||
const aKnownDate = new Date('2023-01-24T14:00Z')
|
||||
const aKnownDate = new Date('2023-01-24T14:00Z');
|
||||
|
||||
// WHEN
|
||||
const photo = controller.getSunriseSunsetPhotoForDate(aKnownDate)
|
||||
const photo = controller.getSunriseSunsetPhotoForDate(aKnownDate);
|
||||
|
||||
// THEN
|
||||
expect(photo).toStrictEqual({
|
||||
|
|
@ -30,17 +27,17 @@ describe('SunriseSunsetController', () => {
|
|||
'https://images.unsplash.com/photo-1475656106224-d72c2ab53e8d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=Mnw0MDEyNTV8MHwxfHNlYXJjaHw5M3x8c3VucmlzZXxlbnwwfHx8fDE2NzQ1MDI4MzQ&ixlib=rb-4.0.3&q=80&w=400',
|
||||
sunrise_or_sunset: 'sunrise',
|
||||
},
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return null when there is no photo for the day`, () => {
|
||||
// GIVEN
|
||||
const aDateWithoutPhoto = new Date('2020-01-01T00:00Z')
|
||||
const aDateWithoutPhoto = new Date('2020-01-01T00:00Z');
|
||||
|
||||
// WHEN
|
||||
const photo = controller.getSunriseSunsetPhotoForDate(aDateWithoutPhoto)
|
||||
const photo = controller.getSunriseSunsetPhotoForDate(aDateWithoutPhoto);
|
||||
|
||||
// THEN
|
||||
expect(photo).toBeNull()
|
||||
})
|
||||
})
|
||||
expect(photo).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
import data from './data.json'
|
||||
import { format as formatDate } from 'date-fns'
|
||||
import data from './data.json';
|
||||
import { format as formatDate } from 'date-fns';
|
||||
|
||||
type Daytime = 'sunrise' | 'sunset'
|
||||
type Daytime = 'sunrise' | 'sunset';
|
||||
interface DailyPhoto {
|
||||
date: string // e.g. "2023-01-24"
|
||||
date: string; // e.g. "2023-01-24"
|
||||
photo: {
|
||||
id: string
|
||||
description: string
|
||||
username: string
|
||||
username_url: string
|
||||
small_url: string
|
||||
sunrise_or_sunset: Daytime
|
||||
}
|
||||
id: string;
|
||||
description: string;
|
||||
username: string;
|
||||
username_url: string;
|
||||
small_url: string;
|
||||
sunrise_or_sunset: Daytime;
|
||||
};
|
||||
}
|
||||
export interface ISunriseSunsetController {
|
||||
getSunriseSunsetPhotoForDate(date: Date): DailyPhoto | null
|
||||
getSunriseSunsetPhotoForDate(date: Date): DailyPhoto | null;
|
||||
}
|
||||
|
||||
export class SunriseSunsetController implements ISunriseSunsetController {
|
||||
private data: DailyPhoto[] = data.photos as any
|
||||
private data: DailyPhoto[] = data.photos as any;
|
||||
|
||||
getSunriseSunsetPhotoForDate(date) {
|
||||
const formattedDate = formatDate(date, 'yyyy-MM-dd')
|
||||
const formattedDate = formatDate(date, 'yyyy-MM-dd');
|
||||
|
||||
return this.data.find((photo) => photo.date === formattedDate) ?? null
|
||||
return this.data.find((photo) => photo.date === formattedDate) ?? null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@ import { json } from '@sveltejs/kit';
|
|||
import wainwrights from '../../../content/wainwrights/wainwrights.json';
|
||||
|
||||
export const GET = async ({ url }) => {
|
||||
try {
|
||||
return json({
|
||||
wainwrights
|
||||
});
|
||||
} catch (error) {
|
||||
console.error({ error: JSON.stringify(error) });
|
||||
return json(
|
||||
{
|
||||
error: 'Could not fetch wainwrights' + error
|
||||
},
|
||||
{
|
||||
status: 500
|
||||
}
|
||||
);
|
||||
}
|
||||
try {
|
||||
return json({
|
||||
wainwrights,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error({ error: JSON.stringify(error) });
|
||||
return json(
|
||||
{
|
||||
error: 'Could not fetch wainwrights' + error,
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,54 +1,54 @@
|
|||
import {
|
||||
BlogController,
|
||||
type BlogItem,
|
||||
type BlogPostListItem,
|
||||
type BookReviewListItem,
|
||||
} from "$lib/blog/BlogController.js";
|
||||
import type { BookReview } from "$lib/blog/BookReview.js";
|
||||
import type { Load } from "@sveltejs/kit";
|
||||
import { differenceInCalendarDays, getYear } from "date-fns";
|
||||
BlogController,
|
||||
type BlogItem,
|
||||
type BlogPostListItem,
|
||||
type BookReviewListItem,
|
||||
} from '$lib/blog/BlogController.js';
|
||||
import type { BookReview } from '$lib/blog/BookReview.js';
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { differenceInCalendarDays, getYear } from 'date-fns';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
type PostsGroupedByMonth = Array<{
|
||||
yearDate: string;
|
||||
posts: (BlogPostListItem | BookReviewListItem)[];
|
||||
yearDate: string;
|
||||
posts: (BlogPostListItem | BookReviewListItem)[];
|
||||
}>;
|
||||
|
||||
export const load: Load = async ({}) => {
|
||||
const controller = await BlogController.singleton();
|
||||
const posts = await controller.getAllBlogPosts();
|
||||
const controller = await BlogController.singleton();
|
||||
const posts = await controller.getAllBlogPosts();
|
||||
|
||||
const currentYear = getYear(new Date());
|
||||
const currentYear = getYear(new Date());
|
||||
|
||||
const numberOfPosts = posts.length;
|
||||
const firstPost = posts[numberOfPosts - 1];
|
||||
const numberOfBlogPostsThisYear: number = posts.filter(
|
||||
(post) => getYear(new Date(post.date)) === currentYear,
|
||||
).length;
|
||||
const numberOfPosts = posts.length;
|
||||
const firstPost = posts[numberOfPosts - 1];
|
||||
const numberOfBlogPostsThisYear: number = posts.filter(
|
||||
(post) => getYear(new Date(post.date)) === currentYear
|
||||
).length;
|
||||
|
||||
const postsGroupedByMonth = posts.reduce((grouped, post) => {
|
||||
const yearDate = Intl.DateTimeFormat("en-gb", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
}).format(new Date(post.date));
|
||||
const postsGroupedByMonth = posts.reduce((grouped, post) => {
|
||||
const yearDate = Intl.DateTimeFormat('en-gb', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
}).format(new Date(post.date));
|
||||
|
||||
const index = grouped.findIndex((entry) => entry.yearDate === yearDate);
|
||||
const index = grouped.findIndex((entry) => entry.yearDate === yearDate);
|
||||
|
||||
if (index === -1) {
|
||||
grouped.push({ yearDate, posts: [post] });
|
||||
} else {
|
||||
grouped[index].posts.push(post);
|
||||
}
|
||||
if (index === -1) {
|
||||
grouped.push({ yearDate, posts: [post] });
|
||||
} else {
|
||||
grouped[index].posts.push(post);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}, [] as PostsGroupedByMonth);
|
||||
return grouped;
|
||||
}, [] as PostsGroupedByMonth);
|
||||
|
||||
return {
|
||||
posts,
|
||||
postsGroupedByMonth,
|
||||
firstPost,
|
||||
numberOfPosts,
|
||||
numberOfBlogPostsThisYear,
|
||||
};
|
||||
return {
|
||||
posts,
|
||||
postsGroupedByMonth,
|
||||
firstPost,
|
||||
numberOfPosts,
|
||||
numberOfBlogPostsThisYear,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
import { access, readFile } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
|
||||
import * as path from "node:path";
|
||||
import { error, type ServerLoad } from "@sveltejs/kit";
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import { constants } from 'node:fs';
|
||||
import { PRIVATE_PHOTO_UPLOAD_DIR } from '$env/static/private';
|
||||
import * as path from 'node:path';
|
||||
import { error, type ServerLoad } from '@sveltejs/kit';
|
||||
|
||||
export const GET: ServerLoad = async ({ params, locals }) => {
|
||||
const { filename } = params;
|
||||
const { filename } = params;
|
||||
|
||||
const proposedFilePath = path.join(PRIVATE_PHOTO_UPLOAD_DIR, filename);
|
||||
const proposedFilePath = path.join(PRIVATE_PHOTO_UPLOAD_DIR, filename);
|
||||
|
||||
const fileExists = await access(proposedFilePath, constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
const fileExists = await access(proposedFilePath, constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!fileExists) {
|
||||
return error(404, "File not found");
|
||||
}
|
||||
if (!fileExists) {
|
||||
console.warn(`File with name ${filename} not found.`);
|
||||
return error(404, 'File not found');
|
||||
}
|
||||
|
||||
const file = await readFile(proposedFilePath);
|
||||
const fileExt = path.extname(filename);
|
||||
const contentType = `image/${fileExt.replace(".", "")}`;
|
||||
const file = await readFile(proposedFilePath);
|
||||
const fileExt = path.extname(filename);
|
||||
const contentType = `image/${fileExt.replace('.', '')}`;
|
||||
|
||||
return new Response(file, { headers: { "Content-Type": contentType } });
|
||||
return new Response(file, { headers: { 'Content-Type': contentType } });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import type { PageServerLoad } from "./$types.js";
|
||||
import type { PageServerLoad } from './$types.js';
|
||||
|
||||
export const load: PageServerLoad = async({}) => {
|
||||
return {
|
||||
|
||||
}
|
||||
}
|
||||
export const load: PageServerLoad = async ({}) => {
|
||||
return {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import type { PageServerLoad } from "./$types.js";
|
||||
import type { PageServerLoad } from './$types.js';
|
||||
|
||||
export const load: PageServerLoad = async({}) => {
|
||||
return {
|
||||
|
||||
}
|
||||
}
|
||||
export const load: PageServerLoad = async ({}) => {
|
||||
return {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import type { LoadEvent } from '@sveltejs/kit';
|
|||
import type { Wainwright } from './Wainwright.js';
|
||||
|
||||
export async function load({ fetch }: LoadEvent): Promise<{ wainwrights: Wainwright[] }> {
|
||||
const { wainwrights } = await fetch(`/api/wainwrights.json`)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return { wainwrights: [] };
|
||||
});
|
||||
const { wainwrights } = await fetch(`/api/wainwrights.json`)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return { wainwrights: [] };
|
||||
});
|
||||
|
||||
return {
|
||||
wainwrights
|
||||
};
|
||||
return {
|
||||
wainwrights,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
export interface Wainwright {
|
||||
number: number;
|
||||
name: string;
|
||||
classification: string;
|
||||
isWainwright: boolean;
|
||||
heightMetres: number;
|
||||
heightFeet: number;
|
||||
dropMetres: number;
|
||||
colMetres: number;
|
||||
osGridRef: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
number: number;
|
||||
name: string;
|
||||
classification: string;
|
||||
isWainwright: boolean;
|
||||
heightMetres: number;
|
||||
heightFeet: number;
|
||||
dropMetres: number;
|
||||
colMetres: number;
|
||||
osGridRef: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,105 @@
|
|||
/* PrismJS 1.29.0
|
||||
https://prismjs.com/download.html#themes=prism-solarizedlight&languages=markup+css+clike+javascript */
|
||||
code[class*=language-],pre[class*=language-]{color:#657b83;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{background:#073642}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{background:#073642}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background-color:#fdf6e3}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#93a1a1}.token.punctuation{color:#586e75}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#268bd2}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string,.token.url{color:#2aa198}.token.entity{color:#657b83;background:#eee8d5}.token.atrule,.token.attr-value,.token.keyword{color:#859900}.token.class-name,.token.function{color:#b58900}.token.important,.token.regex,.token.variable{color:#cb4b16}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
color: #657b83;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
code[class*='language-'] ::-moz-selection,
|
||||
code[class*='language-']::-moz-selection,
|
||||
pre[class*='language-'] ::-moz-selection,
|
||||
pre[class*='language-']::-moz-selection {
|
||||
background: #073642;
|
||||
}
|
||||
code[class*='language-'] ::selection,
|
||||
code[class*='language-']::selection,
|
||||
pre[class*='language-'] ::selection,
|
||||
pre[class*='language-']::selection {
|
||||
background: #073642;
|
||||
}
|
||||
pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
:not(pre) > code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
background-color: #fdf6e3;
|
||||
}
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
.token.cdata,
|
||||
.token.comment,
|
||||
.token.doctype,
|
||||
.token.prolog {
|
||||
color: #93a1a1;
|
||||
}
|
||||
.token.punctuation {
|
||||
color: #586e75;
|
||||
}
|
||||
.token.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.token.boolean,
|
||||
.token.constant,
|
||||
.token.deleted,
|
||||
.token.number,
|
||||
.token.property,
|
||||
.token.symbol,
|
||||
.token.tag {
|
||||
color: #268bd2;
|
||||
}
|
||||
.token.attr-name,
|
||||
.token.builtin,
|
||||
.token.char,
|
||||
.token.inserted,
|
||||
.token.selector,
|
||||
.token.string,
|
||||
.token.url {
|
||||
color: #2aa198;
|
||||
}
|
||||
.token.entity {
|
||||
color: #657b83;
|
||||
background: #eee8d5;
|
||||
}
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #859900;
|
||||
}
|
||||
.token.class-name,
|
||||
.token.function {
|
||||
color: #b58900;
|
||||
}
|
||||
.token.important,
|
||||
.token.regex,
|
||||
.token.variable {
|
||||
color: #cb4b16;
|
||||
}
|
||||
.token.bold,
|
||||
.token.important {
|
||||
font-weight: 700;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,123 +1,126 @@
|
|||
@font-face {
|
||||
font-family: "FivoSansModern-Regular";
|
||||
src: url("/FivoSansModern-Regular.otf");
|
||||
font-display: swap;
|
||||
font-family: 'FivoSansModern-Regular';
|
||||
src: url('/FivoSansModern-Regular.otf');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--brand-orange: #ff8c0d;
|
||||
--brand-purple: #464d77;
|
||||
--brand-green: #36827f;
|
||||
--brand-blue: #00a0e9;
|
||||
--white: #fff;
|
||||
--gray-100: #f8f9fa;
|
||||
--gray-200: #e9ecef;
|
||||
--gray-300: #dee2e6;
|
||||
--gray-400: #ced4da;
|
||||
--gray-500: #adb5bd;
|
||||
--gray-600: #6c757d;
|
||||
--gray-700: #495057;
|
||||
--gray-800: #343a40;
|
||||
--gray-900: #212529;
|
||||
--gray-950: #1a1e23;
|
||||
--gray-1000: #0a0c0e;
|
||||
--font-family-mono: monospace;
|
||||
--font-family-title: "FivoSansModern-Regular", sans-serif;
|
||||
--font-family-sans:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
||||
Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-family-serif: Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--brand-orange: #ff8c0d;
|
||||
--brand-purple: #464d77;
|
||||
--brand-green: #36827f;
|
||||
--brand-blue: #00a0e9;
|
||||
--white: #fff;
|
||||
--gray-100: #f8f9fa;
|
||||
--gray-200: #e9ecef;
|
||||
--gray-300: #dee2e6;
|
||||
--gray-400: #ced4da;
|
||||
--gray-500: #adb5bd;
|
||||
--gray-600: #6c757d;
|
||||
--gray-700: #495057;
|
||||
--gray-800: #343a40;
|
||||
--gray-900: #212529;
|
||||
--gray-950: #1a1e23;
|
||||
--gray-1000: #0a0c0e;
|
||||
|
||||
--line-height: 120%;
|
||||
--line-height-sm: 120%;
|
||||
--line-height-md: 140%;
|
||||
--line-height-lg: 145%;
|
||||
--colour-danger: red;
|
||||
--font-family-mono: monospace;
|
||||
--font-family-title: 'FivoSansModern-Regular', sans-serif;
|
||||
--font-family-sans:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-family-serif: Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
|
||||
--font-size-xs: 11px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.25rem;
|
||||
--font-size-xl: 1.5rem;
|
||||
--font-size-2xl: 2rem;
|
||||
--spacing-base: 1rem;
|
||||
--spacing-sm: 4px;
|
||||
--spacing-md: 8px;
|
||||
--spacing-lg: 12px;
|
||||
--spacing-xl: 16px;
|
||||
--navbar-height: 75px;
|
||||
--line-height: 120%;
|
||||
--line-height-sm: 120%;
|
||||
--line-height-md: 140%;
|
||||
--line-height-lg: 145%;
|
||||
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size: 1.12rem;
|
||||
--font-size-md: 1.25rem;
|
||||
--font-size-lg: 1.5rem;
|
||||
--border-radius-sm: 4px;
|
||||
--border-radius-md: 8px;
|
||||
--border-radius-lg: 12px;
|
||||
|
||||
--btn-border: 0;
|
||||
--btn-padding: var(--spacing-sm);
|
||||
--btn-border-radius: 0.25rem;
|
||||
--btn-font-size: 1.08rem;
|
||||
--btn-text-decoration: none;
|
||||
--font-size-xs: 11px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.25rem;
|
||||
--font-size-xl: 1.5rem;
|
||||
--font-size-2xl: 2rem;
|
||||
--spacing-base: 1rem;
|
||||
--spacing-sm: 4px;
|
||||
--spacing-md: 8px;
|
||||
--spacing-lg: 12px;
|
||||
--spacing-xl: 16px;
|
||||
--navbar-height: 75px;
|
||||
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size: 1.12rem;
|
||||
--font-size-md: 1.25rem;
|
||||
--font-size-lg: 1.5rem;
|
||||
|
||||
--btn-border: 0;
|
||||
--btn-padding: var(--spacing-sm);
|
||||
--btn-border-radius: 0.25rem;
|
||||
--btn-font-size: 1.08rem;
|
||||
--btn-text-decoration: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-family: var(--font-family-sans);
|
||||
line-height: var(--line-height-md);
|
||||
color: var(--colour-scheme-text);
|
||||
background-color: var(--colour-scheme-background, black);
|
||||
transition: 0.3s ease;
|
||||
transition-property: background-color, color;
|
||||
font-size: 16px;
|
||||
font-family: var(--font-family-sans);
|
||||
line-height: var(--line-height-md);
|
||||
color: var(--colour-scheme-text);
|
||||
background-color: var(--colour-scheme-background, black);
|
||||
transition: 0.3s ease;
|
||||
transition-property: background-color, color;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family-sans);
|
||||
line-height: var(--line-height-md);
|
||||
min-height: 100vh;
|
||||
background-color: var(--colour-scheme-background, black);
|
||||
color: var(--colour-scheme-text);
|
||||
transition: 0.3s ease;
|
||||
transition-property: background-color, color;
|
||||
font-family: var(--font-family-sans);
|
||||
line-height: var(--line-height-md);
|
||||
min-height: 100vh;
|
||||
background-color: var(--colour-scheme-background, black);
|
||||
color: var(--colour-scheme-text);
|
||||
transition: 0.3s ease;
|
||||
transition-property: background-color, color;
|
||||
}
|
||||
|
||||
.thomaswilson-container {
|
||||
--container-padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(
|
||||
100vh - var(--navbar-height) - calc(2 * var(--container-padding))
|
||||
);
|
||||
padding: var(--container-padding);
|
||||
--container-padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - var(--navbar-height) - calc(2 * var(--container-padding)));
|
||||
padding: var(--container-padding);
|
||||
}
|
||||
|
||||
.thomaswilson-container .section {
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
max-width: 750px;
|
||||
font-size: 1.19rem;
|
||||
line-height: var(--line-height-md);
|
||||
padding-bottom: var(--spacing-base);
|
||||
padding-bottom: 2rem;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
max-width: 750px;
|
||||
font-size: 1.19rem;
|
||||
line-height: var(--line-height-md);
|
||||
padding-bottom: var(--spacing-base);
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.thomaswilson-strapline .title {
|
||||
font-family: var(--font-family-title);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
font-family: var(--font-family-title);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.thomaswilson-strapline p {
|
||||
font-size: var(--font-size-md);
|
||||
line-height: var(--line-height-md);
|
||||
letter-spacing: -0.25px;
|
||||
font-weight: 200;
|
||||
font-size: var(--font-size-md);
|
||||
line-height: var(--line-height-md);
|
||||
letter-spacing: -0.25px;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@container (width < 500px) {
|
||||
.thomaswilson-strapline p {
|
||||
}
|
||||
.thomaswilson-strapline p {
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
|
|
@ -126,137 +129,245 @@ h3,
|
|||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-family-title);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--colour-scheme-text);
|
||||
padding-top: 12px;
|
||||
padding-bottom: 8px;
|
||||
line-height: var(--line-height);
|
||||
letter-spacing: 1.5px;
|
||||
font-family: var(--font-family-title);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--colour-scheme-text);
|
||||
padding-top: 12px;
|
||||
padding-bottom: 8px;
|
||||
line-height: var(--line-height);
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.25rem;
|
||||
padding-top: 0.7rem;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 2.25rem;
|
||||
padding-top: 0.7rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p,
|
||||
li,
|
||||
a,
|
||||
blockquote {
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-height-lg);
|
||||
font-family: var(--font-family-mono);
|
||||
margin: 0;
|
||||
color: var(--colour-scheme-text);
|
||||
padding: 4px 0;
|
||||
letter-spacing: 0px;
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-height-lg);
|
||||
font-family: var(--font-family-mono);
|
||||
margin: 0;
|
||||
color: var(--colour-scheme-text);
|
||||
padding: 4px 0;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-style: solid;
|
||||
padding: 0.25rem 0 0.5rem 1rem;
|
||||
border-width: 0px;
|
||||
border-left: 4px solid var(--brand-orange);
|
||||
opacity: 0.85;
|
||||
max-width: 60ch;
|
||||
border-style: solid;
|
||||
padding: 0.25rem 0 0.5rem 1rem;
|
||||
border-width: 0px;
|
||||
border-left: 4px solid var(--brand-orange);
|
||||
opacity: 0.85;
|
||||
max-width: 60ch;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--spacing-base);
|
||||
padding-left: var(--spacing-base);
|
||||
}
|
||||
|
||||
.thomaswilson-button {
|
||||
border: var(--btn-border);
|
||||
padding: var(--btn-padding);
|
||||
border-radius: var(--btn-border-radius);
|
||||
font-size: var(--btn-font-size);
|
||||
text-decoration: var(--btn-text-decoration);
|
||||
border: var(--btn-border);
|
||||
padding: var(--btn-padding);
|
||||
border-radius: var(--btn-border-radius);
|
||||
font-size: var(--btn-font-size);
|
||||
text-decoration: var(--btn-text-decoration);
|
||||
}
|
||||
|
||||
.thomaswilson-button.danger {
|
||||
color: var(--colour-danger);
|
||||
border: 1px solid var(--colour-danger);
|
||||
}
|
||||
|
||||
.thomaswilson-button:hover {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a::after {
|
||||
content: url("/assets/icons/link.svg");
|
||||
margin-left: 3px;
|
||||
content: url('/assets/icons/link.svg');
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
sup a::after {
|
||||
content: none;
|
||||
margin-left: 1px;
|
||||
content: none;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
a.no-icon::after {
|
||||
content: "";
|
||||
display: none;
|
||||
content: '';
|
||||
display: none;
|
||||
}
|
||||
|
||||
a.breadcrumb {
|
||||
color: var(--gray-600);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
a.breadcrumb::after {
|
||||
content: '';
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* An Alert-like component*/
|
||||
.alert {
|
||||
--colour-scheme-border: var(--gray-800);
|
||||
--font-size: var(--font-size-base);
|
||||
--colour-scheme-text: var(--gray-800);
|
||||
--colour-scheme-bg: var(--gray-100);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--colour-scheme-border);
|
||||
border-radius: 4px;
|
||||
font-size: var(--font-size);
|
||||
letter-spacing: 0px;
|
||||
color: var(--colour-scheme-text);
|
||||
background-color: var(--colour-scheme-bg);
|
||||
--colour-scheme-border: var(--gray-800);
|
||||
--font-size: var(--font-size-base);
|
||||
--colour-scheme-text: var(--gray-800);
|
||||
--colour-scheme-bg: var(--gray-100);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--colour-scheme-border);
|
||||
border-radius: 4px;
|
||||
font-size: var(--font-size);
|
||||
letter-spacing: 0px;
|
||||
color: var(--colour-scheme-text);
|
||||
background-color: var(--colour-scheme-bg);
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
--colour-scheme-border: var(--red-800);
|
||||
--colour-scheme-text: var(--red-800);
|
||||
--colour-scheme-bg: var(--red-100);
|
||||
--colour-scheme-border: var(--red-800);
|
||||
--colour-scheme-text: var(--red-800);
|
||||
--colour-scheme-bg: var(--red-100);
|
||||
}
|
||||
|
||||
.admin-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.admin-form .field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.admin-form .field label {
|
||||
font-size: var(--font-size);
|
||||
letter-spacing: 0px;
|
||||
color: var(--colour-scheme-text);
|
||||
font-size: var(--font-size);
|
||||
letter-spacing: 0px;
|
||||
color: var(--colour-scheme-text);
|
||||
}
|
||||
.admin-form .field input {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--colour-scheme-border);
|
||||
border-radius: 4px;
|
||||
font-size: var(--font-size);
|
||||
letter-spacing: 0px;
|
||||
color: var(--colour-scheme-text);
|
||||
background-color: var(--colour-scheme-bg);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--colour-scheme-border);
|
||||
border-radius: 4px;
|
||||
font-size: var(--font-size);
|
||||
letter-spacing: 0px;
|
||||
color: var(--colour-scheme-text);
|
||||
background-color: var(--colour-scheme-bg);
|
||||
}
|
||||
|
||||
dialog.confirm-dialog {
|
||||
border: 1px solid var(--colour-scheme-border);
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
background-color: var(--colour-scheme-background);
|
||||
background-color: var(--colour-scheme-background);
|
||||
color: var(--colour-scheme-text);
|
||||
}
|
||||
|
||||
.confirm-dialog .delete-button {
|
||||
border: 1px solid var(--colour-danger);
|
||||
background-color: var(--colour-scheme-background);
|
||||
color: var(--colour-danger);
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
background-color: #ffe3e3;
|
||||
}
|
||||
|
||||
.delete-dialog::backdrop {
|
||||
background: rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* SECTION: CMS
|
||||
*/
|
||||
|
||||
.cms-form {
|
||||
--form-max-width: 760px;
|
||||
--form-border: var(--colour-scheme-border, var(--gray-300));
|
||||
--button-bg: var(--gray-900);
|
||||
--button-text: var(--white);
|
||||
|
||||
width: min(100%, var(--form-max-width));
|
||||
padding: var(--spacing-base);
|
||||
border: 1px solid var(--form-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-base);
|
||||
}>
|
||||
|
||||
.cms-form .field {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.cms-form :is(input[type="text"], textarea, input[type="file"]) {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: var(--spacing-md) var(--spacing-base);
|
||||
border: 1px solid var(--form-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--colour-scheme-bg);
|
||||
color: var(--colour-scheme-text);
|
||||
}
|
||||
|
||||
.cms-form textarea {
|
||||
min-height: 7rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cms-form {
|
||||
padding: 0.85rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
118
static/normalize.css
vendored
118
static/normalize.css
vendored
|
|
@ -9,8 +9,8 @@
|
|||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
|
|
@ -21,7 +21,7 @@ html {
|
|||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -29,7 +29,7 @@ body {
|
|||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -38,8 +38,8 @@ main {
|
|||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
|
|
@ -51,9 +51,9 @@ h1 {
|
|||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,8 +62,8 @@ hr {
|
|||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
|
|
@ -74,7 +74,7 @@ pre {
|
|||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -83,9 +83,9 @@ a {
|
|||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -94,7 +94,7 @@ abbr[title] {
|
|||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,8 +105,8 @@ strong {
|
|||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -114,7 +114,7 @@ samp {
|
|||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,18 +124,18 @@ small {
|
|||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
|
|
@ -146,7 +146,7 @@ sup {
|
|||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
|
|
@ -162,10 +162,10 @@ input,
|
|||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -175,8 +175,8 @@ textarea {
|
|||
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -186,8 +186,8 @@ input {
|
|||
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -198,7 +198,7 @@ button,
|
|||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -209,8 +209,8 @@ button::-moz-focus-inner,
|
|||
[type='button']::-moz-focus-inner,
|
||||
[type='reset']::-moz-focus-inner,
|
||||
[type='submit']::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -221,7 +221,7 @@ button:-moz-focusring,
|
|||
[type='button']:-moz-focusring,
|
||||
[type='reset']:-moz-focusring,
|
||||
[type='submit']:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -229,7 +229,7 @@ button:-moz-focusring,
|
|||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -240,12 +240,12 @@ fieldset {
|
|||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -253,7 +253,7 @@ legend {
|
|||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -261,7 +261,7 @@ progress {
|
|||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -271,8 +271,8 @@ textarea {
|
|||
|
||||
[type='checkbox'],
|
||||
[type='radio'] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -281,7 +281,7 @@ textarea {
|
|||
|
||||
[type='number']::-webkit-inner-spin-button,
|
||||
[type='number']::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -290,8 +290,8 @@ textarea {
|
|||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -299,7 +299,7 @@ textarea {
|
|||
*/
|
||||
|
||||
[type='search']::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -308,8 +308,8 @@ textarea {
|
|||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
|
|
@ -320,7 +320,7 @@ textarea {
|
|||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -328,7 +328,7 @@ details {
|
|||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
|
|
@ -339,7 +339,7 @@ summary {
|
|||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -347,5 +347,5 @@ template {
|
|||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
651
static/prism.js
651
static/prism.js
File diff suppressed because one or more lines are too long
|
|
@ -1,25 +1,25 @@
|
|||
import adapter from "@sveltejs/adapter-node";
|
||||
import preprocess from "svelte-preprocess";
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: [".svelte", ".md"],
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: [preprocess()],
|
||||
extensions: ['.svelte', '.md'],
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: [preprocess()],
|
||||
|
||||
kit: {
|
||||
adapter: adapter({ split: false }),
|
||||
alias: {
|
||||
$lib: "/src/lib",
|
||||
$srcPrisma: "/src/prisma",
|
||||
$generatedPrisma: "/generated/prisma/*",
|
||||
kit: {
|
||||
adapter: adapter({ split: false }),
|
||||
alias: {
|
||||
$lib: '/src/lib',
|
||||
$srcPrisma: '/src/prisma',
|
||||
$generatedPrisma: '/generated/prisma/*',
|
||||
},
|
||||
env: {
|
||||
publicPrefix: 'PUBLIC_',
|
||||
privatePrefix: 'PRIVATE_',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
publicPrefix: "PUBLIC_",
|
||||
privatePrefix: "PRIVATE_",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,8 @@
|
|||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [sveltekit()],
|
||||
resolve: {
|
||||
alias: {
|
||||
$lib: "/src/lib",
|
||||
$srcPrisma: "/src/prisma",
|
||||
},
|
||||
},
|
||||
plugins: [sveltekit()],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export default {
|
|||
alias: {
|
||||
$lib: '/src/lib',
|
||||
$srcPrisma: '/src/prisma',
|
||||
$generatedPrisma: '/generated/prisma',
|
||||
},
|
||||
},
|
||||
test: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue