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