Use backlinks and rework the caching system
This commit is contained in:
parent
f1b37a31a9
commit
7dd91f5847
|
@ -3,7 +3,8 @@ import type {
|
||||||
EndpointWebsiteConfig,
|
EndpointWebsiteConfig,
|
||||||
EndpointWording,
|
EndpointWording,
|
||||||
} from "src/shared/payload/endpoint-types";
|
} from "src/shared/payload/endpoint-types";
|
||||||
import type { PayloadSDK } from "src/shared/payload/sdk";
|
import { SDKEndpointNames, type PayloadSDK } from "src/shared/payload/sdk";
|
||||||
|
import type { EndpointChange } from "src/shared/payload/webhooks";
|
||||||
import { getLogger } from "src/utils/logger";
|
import { getLogger } from "src/utils/logger";
|
||||||
|
|
||||||
export class ContextCache {
|
export class ContextCache {
|
||||||
|
@ -35,24 +36,42 @@ export class ContextCache {
|
||||||
await this.refreshWordings();
|
await this.refreshWordings();
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshWordings() {
|
async invalidate(changes: EndpointChange[]) {
|
||||||
|
for (const change of changes) {
|
||||||
|
switch (change.type) {
|
||||||
|
case SDKEndpointNames.getWordings:
|
||||||
|
return await this.refreshWordings();
|
||||||
|
|
||||||
|
case SDKEndpointNames.getLanguages:
|
||||||
|
return await this.refreshLocales();
|
||||||
|
|
||||||
|
case SDKEndpointNames.getCurrencies:
|
||||||
|
return await this.refreshCurrencies();
|
||||||
|
|
||||||
|
case SDKEndpointNames.getWebsiteConfig:
|
||||||
|
return await this.refreshWebsiteConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshWordings() {
|
||||||
this.wordings = (await this.payload.getWordings()).data;
|
this.wordings = (await this.payload.getWordings()).data;
|
||||||
this.logger.log("Wordings refreshed");
|
this.logger.log("Wordings refreshed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshCurrencies() {
|
private async refreshCurrencies() {
|
||||||
this.currencies = (await this.payload.getCurrencies()).data.map(({ id }) => id);
|
this.currencies = (await this.payload.getCurrencies()).data.map(({ id }) => id);
|
||||||
this.logger.log("Currencies refreshed");
|
this.logger.log("Currencies refreshed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshLocales() {
|
private async refreshLocales() {
|
||||||
this.languages = (await this.payload.getLanguages()).data;
|
this.languages = (await this.payload.getLanguages()).data;
|
||||||
this.locales = this.languages.filter(({ selectable }) => selectable).map(({ id }) => id);
|
this.locales = this.languages.filter(({ selectable }) => selectable).map(({ id }) => id);
|
||||||
this.logger.log("Locales refreshed");
|
this.logger.log("Locales refreshed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshWebsiteConfig() {
|
private async refreshWebsiteConfig() {
|
||||||
this.config = (await this.payload.getConfig()).data;
|
this.config = (await this.payload.getWebsiteConfig()).data;
|
||||||
this.logger.log("WebsiteConfig refreshed");
|
this.logger.log("WebsiteConfig refreshed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { getLogger } from "src/utils/logger";
|
||||||
import { writeFile, mkdir, readFile } from "fs/promises";
|
import { writeFile, mkdir, readFile } from "fs/promises";
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import type { PayloadSDK } from "src/shared/payload/sdk";
|
import type { PayloadSDK } from "src/shared/payload/sdk";
|
||||||
|
import type { EndpointChange } from "src/shared/payload/webhooks";
|
||||||
|
|
||||||
const ON_DISK_ROOT = `.cache/dataCache`;
|
const ON_DISK_ROOT = `.cache/dataCache`;
|
||||||
const ON_DISK_RESPONSE_CACHE_FILE = `${ON_DISK_ROOT}/responseCache.json`;
|
const ON_DISK_RESPONSE_CACHE_FILE = `${ON_DISK_ROOT}/responseCache.json`;
|
||||||
|
@ -11,14 +12,12 @@ export class DataCache {
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
|
|
||||||
private readonly responseCache = new Map<string, any>();
|
private readonly responseCache = new Map<string, any>();
|
||||||
private readonly invalidationMap = new Map<string, Set<string>>();
|
|
||||||
|
|
||||||
private scheduleSaveTimeout: NodeJS.Timeout | undefined;
|
private scheduleSaveTimeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly payload: PayloadSDK,
|
private readonly payload: PayloadSDK,
|
||||||
private readonly uncachedPayload: PayloadSDK,
|
private readonly uncachedPayload: PayloadSDK
|
||||||
private readonly onInvalidate: (urls: string[]) => Promise<void>
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
@ -32,8 +31,8 @@ export class DataCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async precache() {
|
private async precache() {
|
||||||
// Get all keys from CMS
|
// Get all documents from CMS
|
||||||
const allSDKUrls = (await this.uncachedPayload.getAllSdkUrls()).data.urls;
|
const allDocs = (await this.uncachedPayload.getAll()).data;
|
||||||
|
|
||||||
// Load cache from disk if available
|
// Load cache from disk if available
|
||||||
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE)) {
|
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE)) {
|
||||||
|
@ -42,20 +41,20 @@ export class DataCache {
|
||||||
const data = JSON.parse(buffer.toString()) as [string, any][];
|
const data = JSON.parse(buffer.toString()) as [string, any][];
|
||||||
for (const [key, value] of data) {
|
for (const [key, value] of data) {
|
||||||
// Do not include cache where the key is no longer in the CMS
|
// Do not include cache where the key is no longer in the CMS
|
||||||
if (!allSDKUrls.includes(key)) continue;
|
if (!allDocs.find(({ url }) => url === key)) continue;
|
||||||
this.set(key, value);
|
this.set(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheSizeBeforePrecaching = this.responseCache.size;
|
const cacheSizeBeforePrecaching = this.responseCache.size;
|
||||||
|
|
||||||
for (const url of allSDKUrls) {
|
for (const doc of allDocs) {
|
||||||
// Do not precache response if already included in the loaded cache from disk
|
// Do not precache response if already included in the loaded cache from disk
|
||||||
if (this.responseCache.has(url)) continue;
|
if (this.responseCache.has(doc.url)) continue;
|
||||||
try {
|
try {
|
||||||
await this.payload.request(url);
|
await this.payload.request(doc.url);
|
||||||
} catch {
|
} catch {
|
||||||
this.logger.warn("Precaching failed for url", url);
|
this.logger.warn("Precaching failed for url", doc.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,20 +76,6 @@ export class DataCache {
|
||||||
|
|
||||||
set(url: string, response: any) {
|
set(url: string, response: any) {
|
||||||
if (import.meta.env.DATA_CACHING !== "true") return;
|
if (import.meta.env.DATA_CACHING !== "true") return;
|
||||||
const stringData = JSON.stringify(response);
|
|
||||||
const regex = /[a-f0-9]{24}/g;
|
|
||||||
const ids = [...stringData.matchAll(regex)].map((match) => match[0]);
|
|
||||||
const uniqueIds = [...new Set(ids)];
|
|
||||||
|
|
||||||
uniqueIds.forEach((id) => {
|
|
||||||
const current = this.invalidationMap.get(id);
|
|
||||||
if (current) {
|
|
||||||
current.add(url);
|
|
||||||
} else {
|
|
||||||
this.invalidationMap.set(id, new Set([url]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.responseCache.set(url, response);
|
this.responseCache.set(url, response);
|
||||||
this.logger.log("Cached response for", url);
|
this.logger.log("Cached response for", url);
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
|
@ -98,18 +83,11 @@ export class DataCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async invalidate(ids: string[], urls: string[]) {
|
async invalidate(changes: EndpointChange[]) {
|
||||||
if (import.meta.env.DATA_CACHING !== "true") return;
|
if (import.meta.env.DATA_CACHING !== "true") return;
|
||||||
const urlsToInvalidate = new Set<string>(urls);
|
|
||||||
|
|
||||||
ids.forEach((id) => {
|
const urls = changes.map(({ url }) => url);
|
||||||
const urlsForThisId = this.invalidationMap.get(id);
|
for (const url of urls) {
|
||||||
if (!urlsForThisId) return;
|
|
||||||
this.invalidationMap.delete(id);
|
|
||||||
[...urlsForThisId].forEach((url) => urlsToInvalidate.add(url));
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const url of urlsToInvalidate) {
|
|
||||||
this.responseCache.delete(url);
|
this.responseCache.delete(url);
|
||||||
this.logger.log("Invalidated cache for", url);
|
this.logger.log("Invalidated cache for", url);
|
||||||
try {
|
try {
|
||||||
|
@ -118,8 +96,6 @@ export class DataCache {
|
||||||
this.logger.log("Revalidation fails for", url);
|
this.logger.log("Revalidation fails for", url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onInvalidate([...urlsToInvalidate]);
|
|
||||||
this.logger.log("There are currently", this.responseCache.size, "responses in cache.");
|
this.logger.log("There are currently", this.responseCache.size, "responses in cache.");
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
this.scheduleSave();
|
this.scheduleSave();
|
||||||
|
|
|
@ -6,22 +6,25 @@ import {
|
||||||
serializeResponse,
|
serializeResponse,
|
||||||
type SerializableResponse,
|
type SerializableResponse,
|
||||||
} from "src/utils/responses";
|
} from "src/utils/responses";
|
||||||
import type { PayloadSDK } from "src/shared/payload/sdk";
|
import { SDKEndpointNames, type PayloadSDK } from "src/shared/payload/sdk";
|
||||||
|
import type { EndpointChange } from "src/shared/payload/webhooks";
|
||||||
|
import type { ContextCache } from "src/cache/contextCache";
|
||||||
|
|
||||||
const ON_DISK_ROOT = `.cache/pageCache`;
|
const ON_DISK_ROOT = `.cache/pageCache`;
|
||||||
const ON_DISK_RESPONSE_CACHE_FILE = `${ON_DISK_ROOT}/responseCache.json`;
|
const ON_DISK_RESPONSE_CACHE_FILE = `${ON_DISK_ROOT}/responseCache.json`;
|
||||||
const ON_DISK_INVALIDATION_MAP_FILE = `${ON_DISK_ROOT}/invalidationMap.json`;
|
|
||||||
|
|
||||||
export class PageCache {
|
export class PageCache {
|
||||||
private readonly logger = getLogger("[PageCache]");
|
private readonly logger = getLogger("[PageCache]");
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
|
|
||||||
private responseCache = new Map<string, Response>();
|
private responseCache = new Map<string, Response>();
|
||||||
private invalidationMap = new Map<string, Set<string>>();
|
|
||||||
|
|
||||||
private scheduleSaveTimeout: NodeJS.Timeout | undefined;
|
private scheduleSaveTimeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
constructor(private readonly uncachedPayload: PayloadSDK) {}
|
constructor(
|
||||||
|
private readonly uncachedPayload: PayloadSDK,
|
||||||
|
private readonly contextCache: ContextCache
|
||||||
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
@ -35,31 +38,16 @@ export class PageCache {
|
||||||
|
|
||||||
private async precache() {
|
private async precache() {
|
||||||
if (import.meta.env.DATA_CACHING !== "true") return;
|
if (import.meta.env.DATA_CACHING !== "true") return;
|
||||||
const { data: languages } = await this.uncachedPayload.getLanguages();
|
|
||||||
const locales = languages.filter(({ selectable }) => selectable).map(({ id }) => id);
|
|
||||||
|
|
||||||
// Get all pages urls from CMS
|
// Get all pages urls from CMS
|
||||||
const allIds = (await this.uncachedPayload.getAllIds()).data;
|
const allDocs = (await this.uncachedPayload.getAll()).data;
|
||||||
|
const allPageUrls = allDocs.flatMap((doc) => this.getUrlFromEndpointChange(doc));
|
||||||
const allPagesUrls = [
|
// TODO: Add static pages likes "/" and "/settings"
|
||||||
"/",
|
|
||||||
...allIds.audios.ids.map((id) => `/audios/${id}`),
|
|
||||||
...allIds.collectibles.slugs.map((slug) => `/collectibles/${slug}`),
|
|
||||||
...allIds.files.ids.map((id) => `/files/${id}`),
|
|
||||||
...allIds.folders.slugs.map((slug) => `/folders/${slug}`),
|
|
||||||
...allIds.images.ids.map((id) => `/images/${id}`),
|
|
||||||
...allIds.pages.slugs.map((slug) => `/pages/${slug}`),
|
|
||||||
...allIds.recorders.ids.map((id) => `/recorders/${id}`),
|
|
||||||
"/settings",
|
|
||||||
"/timeline",
|
|
||||||
...allIds.videos.ids.map((id) => `/videos/${id}`),
|
|
||||||
].flatMap((url) => locales.map((id) => `/${id}${url}`));
|
|
||||||
|
|
||||||
// Load cache from disk if available
|
// Load cache from disk if available
|
||||||
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE) && existsSync(ON_DISK_INVALIDATION_MAP_FILE)) {
|
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE)) {
|
||||||
this.logger.log("Loading cache from disk...");
|
this.logger.log("Loading cache from disk...");
|
||||||
// Handle RESPONSE_CACHE_FILE
|
|
||||||
{
|
|
||||||
const buffer = await readFile(ON_DISK_RESPONSE_CACHE_FILE);
|
const buffer = await readFile(ON_DISK_RESPONSE_CACHE_FILE);
|
||||||
const data = JSON.parse(buffer.toString()) as [string, SerializableResponse][];
|
const data = JSON.parse(buffer.toString()) as [string, SerializableResponse][];
|
||||||
let deserializedData = data.map<[string, Response]>(([key, value]) => [
|
let deserializedData = data.map<[string, Response]>(([key, value]) => [
|
||||||
|
@ -68,26 +56,14 @@ export class PageCache {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Do not include cache where the key is no longer in the CMS
|
// Do not include cache where the key is no longer in the CMS
|
||||||
deserializedData = deserializedData.filter(([key]) => allPagesUrls.includes(key));
|
deserializedData = deserializedData.filter(([key]) => allPageUrls.includes(key));
|
||||||
|
|
||||||
this.responseCache = new Map(deserializedData);
|
this.responseCache = new Map(deserializedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle INVALIDATION_MAP_FILE
|
|
||||||
{
|
|
||||||
const buffer = await readFile(ON_DISK_INVALIDATION_MAP_FILE);
|
|
||||||
const data = JSON.parse(buffer.toString()) as [string, string[]][];
|
|
||||||
const deserialize = data.map<[string, Set<string>]>(([key, value]) => [
|
|
||||||
key,
|
|
||||||
new Set(value),
|
|
||||||
]);
|
|
||||||
this.invalidationMap = new Map(deserialize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheSizeBeforePrecaching = this.responseCache.size;
|
const cacheSizeBeforePrecaching = this.responseCache.size;
|
||||||
|
|
||||||
for (const url of allPagesUrls) {
|
for (const url of allPageUrls) {
|
||||||
// Do not precache response if already included in the loaded cache from disk
|
// Do not precache response if already included in the loaded cache from disk
|
||||||
if (this.responseCache.has(url)) continue;
|
if (this.responseCache.has(url)) continue;
|
||||||
try {
|
try {
|
||||||
|
@ -114,17 +90,8 @@ export class PageCache {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(url: string, response: Response, sdkCalls: string[]) {
|
set(url: string, response: Response) {
|
||||||
if (import.meta.env.PAGE_CACHING !== "true") return;
|
if (import.meta.env.PAGE_CACHING !== "true") return;
|
||||||
sdkCalls.forEach((id) => {
|
|
||||||
const current = this.invalidationMap.get(id);
|
|
||||||
if (current) {
|
|
||||||
current.add(url);
|
|
||||||
} else {
|
|
||||||
this.invalidationMap.set(id, new Set([url]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.responseCache.set(url, response.clone());
|
this.responseCache.set(url, response.clone());
|
||||||
this.logger.log("Cached response for", url);
|
this.logger.log("Cached response for", url);
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
|
@ -132,16 +99,70 @@ export class PageCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async invalidate(sdkUrls: string[]) {
|
private getUrlFromEndpointChange(change: EndpointChange): string[] {
|
||||||
if (import.meta.env.PAGE_CACHING !== "true") return;
|
const getUnlocalizedUrl = (): string[] => {
|
||||||
const pagesToInvalidate = new Set<string>();
|
switch (change.type) {
|
||||||
|
case SDKEndpointNames.getFolder:
|
||||||
|
return [`/folders/${change.slug}`];
|
||||||
|
|
||||||
sdkUrls.forEach((url) => {
|
case SDKEndpointNames.getCollectible:
|
||||||
const pagesForThisSDKUrl = this.invalidationMap.get(url);
|
return [`/collectibles/${change.slug}`];
|
||||||
if (!pagesForThisSDKUrl) return;
|
|
||||||
this.invalidationMap.delete(url);
|
case SDKEndpointNames.getCollectibleGallery:
|
||||||
[...pagesForThisSDKUrl].forEach((page) => pagesToInvalidate.add(page));
|
return [`/collectibles/${change.slug}/gallery`];
|
||||||
});
|
|
||||||
|
case SDKEndpointNames.getCollectibleGalleryImage:
|
||||||
|
return [`/collectibles/${change.slug}/gallery/${change.index}`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getCollectibleScans:
|
||||||
|
return [`/collectibles/${change.slug}/scans`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getCollectibleScanPage:
|
||||||
|
return [`/collectibles/${change.slug}/scans/${change.index}`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getPage:
|
||||||
|
return [`/pages/${change.slug}`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getAudioByID:
|
||||||
|
return [`/audios/${change.id}`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getImageByID:
|
||||||
|
return [`/images/${change.id}`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getVideoByID:
|
||||||
|
return [`/videos/${change.id}`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getFileByID:
|
||||||
|
return [`/files/${change.id}`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getRecorderByID:
|
||||||
|
return [`/recorders/${change.id}`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getChronologyEvents:
|
||||||
|
case SDKEndpointNames.getChronologyEventByID:
|
||||||
|
return [`/timeline`];
|
||||||
|
|
||||||
|
case SDKEndpointNames.getWebsiteConfig:
|
||||||
|
case SDKEndpointNames.getLanguages:
|
||||||
|
case SDKEndpointNames.getCurrencies:
|
||||||
|
case SDKEndpointNames.getWordings:
|
||||||
|
return [...this.responseCache.keys()];
|
||||||
|
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return getUnlocalizedUrl().flatMap((url) =>
|
||||||
|
this.contextCache.locales.map((id) => `/${id}${url}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async invalidate(changes: EndpointChange[]) {
|
||||||
|
if (import.meta.env.PAGE_CACHING !== "true") return;
|
||||||
|
const pagesToInvalidate = new Set<string>(
|
||||||
|
changes.flatMap((change) => this.getUrlFromEndpointChange(change))
|
||||||
|
);
|
||||||
|
|
||||||
for (const url of pagesToInvalidate) {
|
for (const url of pagesToInvalidate) {
|
||||||
this.responseCache.delete(url);
|
this.responseCache.delete(url);
|
||||||
|
@ -181,13 +202,5 @@ export class PageCache {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
});
|
});
|
||||||
this.logger.log("Saved", ON_DISK_RESPONSE_CACHE_FILE);
|
this.logger.log("Saved", ON_DISK_RESPONSE_CACHE_FILE);
|
||||||
|
|
||||||
const serializedIdsCache = JSON.stringify(
|
|
||||||
[...this.invalidationMap].map(([key, value]) => [key, [...value]])
|
|
||||||
);
|
|
||||||
await writeFile(ON_DISK_INVALIDATION_MAP_FILE, serializedIdsCache, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
this.logger.log("Saved", ON_DISK_INVALIDATION_MAP_FILE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@ import Topbar from "./components/Topbar/Topbar.astro";
|
||||||
import Footer from "./components/Footer.astro";
|
import Footer from "./components/Footer.astro";
|
||||||
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
|
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
|
||||||
import type { ComponentProps } from "astro/types";
|
import type { ComponentProps } from "astro/types";
|
||||||
import type { EndpointSource } from "src/shared/payload/endpoint-types";
|
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
||||||
import { getSDKEndpoint } from "src/shared/payload/sdk";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
openGraph?: ComponentProps<typeof Html>["openGraph"];
|
openGraph?: ComponentProps<typeof Html>["openGraph"];
|
||||||
parentPages?: EndpointSource[];
|
backlinks?: EndpointRelation[];
|
||||||
backgroundImage?: ComponentProps<typeof AppLayoutBackgroundImg>["img"] | undefined;
|
backgroundImage?: ComponentProps<typeof AppLayoutBackgroundImg>["img"] | undefined;
|
||||||
hideFooterLinks?: boolean;
|
hideFooterLinks?: boolean;
|
||||||
hideHomeButton?: boolean;
|
hideHomeButton?: boolean;
|
||||||
|
@ -17,13 +16,9 @@ interface Props {
|
||||||
class?: string | undefined;
|
class?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
Astro.locals.sdkCalls.add(getSDKEndpoint.getCurrenciesEndpoint());
|
|
||||||
Astro.locals.sdkCalls.add(getSDKEndpoint.getLanguagesEndpoint());
|
|
||||||
Astro.locals.sdkCalls.add(getSDKEndpoint.getWordingsEndpoint());
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
openGraph,
|
openGraph,
|
||||||
parentPages,
|
backlinks,
|
||||||
backgroundImage,
|
backgroundImage,
|
||||||
hideFooterLinks = false,
|
hideFooterLinks = false,
|
||||||
hideHomeButton = false,
|
hideHomeButton = false,
|
||||||
|
@ -38,7 +33,7 @@ const {
|
||||||
{backgroundImage && <AppLayoutBackgroundImg img={backgroundImage} />}
|
{backgroundImage && <AppLayoutBackgroundImg img={backgroundImage} />}
|
||||||
<header>
|
<header>
|
||||||
<Topbar
|
<Topbar
|
||||||
parentPages={parentPages}
|
backlinks={backlinks}
|
||||||
hideHomeButton={hideHomeButton}
|
hideHomeButton={hideHomeButton}
|
||||||
hideSearchButton={hideSearchButton}
|
hideSearchButton={hideSearchButton}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,15 +6,15 @@ import LanguageSelector from "./components/LanguageSelector.astro";
|
||||||
import CurrencySelector from "./components/CurrencySelector.astro";
|
import CurrencySelector from "./components/CurrencySelector.astro";
|
||||||
import ParentPagesButton from "./components/ParentPagesButton.astro";
|
import ParentPagesButton from "./components/ParentPagesButton.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointSource } from "src/shared/payload/endpoint-types";
|
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parentPages?: EndpointSource[] | undefined;
|
backlinks?: EndpointRelation[] | undefined;
|
||||||
hideHomeButton?: boolean;
|
hideHomeButton?: boolean;
|
||||||
hideSearchButton?: boolean;
|
hideSearchButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { parentPages = [], hideHomeButton = false, hideSearchButton = false } = Astro.props;
|
const { backlinks = [], hideHomeButton = false, hideSearchButton = false } = Astro.props;
|
||||||
|
|
||||||
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||||
---
|
---
|
||||||
|
@ -23,14 +23,14 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
<nav id="topbar" class="when-no-print">
|
<nav id="topbar" class="when-no-print">
|
||||||
{
|
{
|
||||||
(!hideHomeButton || parentPages.length > 0) && (
|
(!hideHomeButton || backlinks.length > 0) && (
|
||||||
<div id="left" class="hide-scrollbar">
|
<div id="left" class="hide-scrollbar">
|
||||||
<a href={getLocalizedUrl("/")} class="pressable-label">
|
<a href={getLocalizedUrl("/")} class="pressable-label">
|
||||||
<Icon name="material-symbols:home" width={16} height={16} />
|
<Icon name="material-symbols:home" width={16} height={16} />
|
||||||
<p>{t("home.title")}</p>
|
<p>{t("home.title")}</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{parentPages.length > 0 && <ParentPagesButton parentPages={parentPages} />}
|
{backlinks.length > 0 && <ParentPagesButton backlinks={backlinks} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,14 @@ import Tooltip from "components/Tooltip.astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import ReturnToButton from "./ReturnToButton.astro";
|
import ReturnToButton from "./ReturnToButton.astro";
|
||||||
import SourceRow from "components/SourceRow.astro";
|
import RelationRow from "components/RelationRow.astro";
|
||||||
import type { EndpointSource } from "src/shared/payload/endpoint-types";
|
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parentPages: EndpointSource[];
|
backlinks: EndpointRelation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { parentPages } = Astro.props;
|
const { backlinks } = Astro.props;
|
||||||
|
|
||||||
const { t } = await getI18n(Astro.locals.currentLocale);
|
const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
---
|
---
|
||||||
|
@ -18,15 +18,15 @@ const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
{
|
{
|
||||||
parentPages.length === 1 && parentPages[0] ? (
|
backlinks.length === 1 && backlinks[0] ? (
|
||||||
<ReturnToButton parentPage={parentPages[0]} />
|
<ReturnToButton relation={backlinks[0]} />
|
||||||
) : (
|
) : (
|
||||||
<Tooltip trigger="click" class="when-js">
|
<Tooltip trigger="click" class="when-js">
|
||||||
<div id="tooltip-content" slot="tooltip-content">
|
<div id="tooltip-content" slot="tooltip-content">
|
||||||
<p>{t("header.nav.parentPages.tooltip")}</p>
|
<p>{t("header.nav.parentPages.tooltip")}</p>
|
||||||
<div>
|
<div>
|
||||||
{parentPages.map((parentPage) => (
|
{backlinks.map((relation) => (
|
||||||
<SourceRow source={parentPage} />
|
<RelationRow relation={relation} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,7 +34,7 @@ const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
<Icon name="material-symbols:keyboard-return" />
|
<Icon name="material-symbols:keyboard-return" />
|
||||||
<p>
|
<p>
|
||||||
{t("header.nav.parentPages.label", {
|
{t("header.nav.parentPages.label", {
|
||||||
count: parentPages.length,
|
count: backlinks.length,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
---
|
---
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointSource } from "src/shared/payload/endpoint-types";
|
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parentPage: EndpointSource;
|
relation: EndpointRelation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { parentPage } = Astro.props;
|
const { relation } = Astro.props;
|
||||||
const { formatEndpointSource } = await getI18n(Astro.locals.currentLocale);
|
const { formatEndpointRelation } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
href,
|
href,
|
||||||
|
@ -17,7 +17,7 @@ const {
|
||||||
target = undefined,
|
target = undefined,
|
||||||
rel = undefined,
|
rel = undefined,
|
||||||
lang,
|
lang,
|
||||||
} = formatEndpointSource(parentPage);
|
} = formatEndpointRelation(relation);
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
import GenericPreview from "components/Previews/GenericPreview.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
import { Collections } from "src/shared/payload/constants";
|
||||||
import type { EndpointFolder } from "src/shared/payload/endpoint-types";
|
import type { EndpointFolder } from "src/shared/payload/endpoint-types";
|
||||||
import type { Attribute } from "src/utils/attributes";
|
import type { Attribute } from "src/utils/attributes";
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ interface Props {
|
||||||
const { getLocalizedUrl, getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
|
const { getLocalizedUrl, getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
folder: { translations, slug, files, sections, parentPages },
|
folder: { translations, slug, files, sections, backlinks },
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const { language, title } = getLocalizedMatch(translations);
|
const { language, title } = getLocalizedMatch(translations);
|
||||||
|
@ -32,9 +33,9 @@ const attributes: Attribute[] = [
|
||||||
{
|
{
|
||||||
icon: "material-symbols:keyboard-return",
|
icon: "material-symbols:keyboard-return",
|
||||||
title: t("global.folders.attributes.parent"),
|
title: t("global.folders.attributes.parent"),
|
||||||
values: parentPages.flatMap((parent) => {
|
values: backlinks.flatMap((link) => {
|
||||||
if (parent.type !== "folder") return [];
|
if (link.type !== Collections.Folders) return [];
|
||||||
const name = getLocalizedMatch(parent.folder.translations).title;
|
const name = getLocalizedMatch(link.value.translations).title;
|
||||||
return { name };
|
return { name };
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
---
|
---
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointSource } from "src/shared/payload/endpoint-types";
|
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
source: EndpointSource;
|
relation: EndpointRelation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { source } = Astro.props;
|
const { relation } = Astro.props;
|
||||||
const { formatEndpointSource } = await getI18n(Astro.locals.currentLocale);
|
const { formatEndpointRelation } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
href,
|
href,
|
||||||
|
@ -16,7 +16,7 @@ const {
|
||||||
target = undefined,
|
target = undefined,
|
||||||
rel = undefined,
|
rel = undefined,
|
||||||
lang,
|
lang,
|
||||||
} = formatEndpointSource(source);
|
} = formatEndpointRelation(relation);
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
|
@ -4,7 +4,6 @@
|
||||||
declare namespace App {
|
declare namespace App {
|
||||||
interface Locals {
|
interface Locals {
|
||||||
currentLocale: string;
|
currentLocale: string;
|
||||||
sdkCalls: Set<string>;
|
|
||||||
pageCaching: boolean;
|
pageCaching: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { WordingKey } from "src/i18n/wordings-keys";
|
import type { WordingKey } from "src/i18n/wordings-keys";
|
||||||
import { contextCache } from "src/services";
|
import { contextCache } from "src/services";
|
||||||
import { capitalize, formatInlineTitle } from "src/utils/format";
|
import { capitalize, formatInlineTitle } from "src/utils/format";
|
||||||
import type { EndpointChronologyEvent, EndpointSource } from "src/shared/payload/endpoint-types";
|
import type { EndpointChronologyEvent, EndpointRelation } from "src/shared/payload/endpoint-types";
|
||||||
|
import { Collections } from "src/shared/payload/constants";
|
||||||
|
|
||||||
export const defaultLocale = "en";
|
export const defaultLocale = "en";
|
||||||
|
|
||||||
|
@ -236,8 +237,8 @@ export const getI18n = async (locale: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEndpointSource = (
|
const formatEndpointRelation = (
|
||||||
source: EndpointSource
|
relation: EndpointRelation
|
||||||
): {
|
): {
|
||||||
href: string;
|
href: string;
|
||||||
typeLabel: string;
|
typeLabel: string;
|
||||||
|
@ -246,98 +247,84 @@ export const getI18n = async (locale: string) => {
|
||||||
target?: string;
|
target?: string;
|
||||||
rel?: string;
|
rel?: string;
|
||||||
} => {
|
} => {
|
||||||
switch (source.type) {
|
switch (relation.type) {
|
||||||
case "url": {
|
case "url":
|
||||||
return {
|
return {
|
||||||
href: source.url,
|
href: relation.url,
|
||||||
typeLabel: t("global.sources.typeLabel.url"),
|
typeLabel: t("global.sources.typeLabel.url"),
|
||||||
label: source.label,
|
label: relation.label,
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
rel: "noopener noreferrer",
|
rel: "noopener noreferrer",
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
case "collectible": {
|
case Collections.Collectibles: {
|
||||||
const rangeLabel = (() => {
|
const getRangeLabel = () => {
|
||||||
switch (source.range?.type) {
|
switch (relation.range?.type) {
|
||||||
case "timestamp":
|
case "timestamp":
|
||||||
return t("global.sources.typeLabel.collectible.range.timestamp", {
|
return t("global.sources.typeLabel.collectible.range.timestamp", {
|
||||||
page: source.range.timestamp,
|
page: relation.range.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
case "page":
|
case "page":
|
||||||
return t("global.sources.typeLabel.collectible.range.page", {
|
return t("global.sources.typeLabel.collectible.range.page", {
|
||||||
page: source.range.page,
|
page: relation.range.page,
|
||||||
});
|
});
|
||||||
|
|
||||||
case "custom":
|
case "custom":
|
||||||
return t("global.sources.typeLabel.collectible.range.custom", {
|
return t("global.sources.typeLabel.collectible.range.custom", {
|
||||||
note: getLocalizedMatch(source.range.translations).note,
|
note: getLocalizedMatch(relation.range.translations).note,
|
||||||
});
|
});
|
||||||
|
|
||||||
case undefined:
|
case undefined:
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
})();
|
};
|
||||||
|
|
||||||
const translation = getLocalizedMatch(source.collectible.translations);
|
const translation = getLocalizedMatch(relation.value.translations);
|
||||||
return {
|
return {
|
||||||
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}`),
|
href: getLocalizedUrl(`/collectibles/${relation.value.slug}`),
|
||||||
typeLabel: t("global.sources.typeLabel.collectible"),
|
typeLabel: t("global.sources.typeLabel.collectible"),
|
||||||
label: formatInlineTitle(translation) + rangeLabel,
|
label: formatInlineTitle(translation) + getRangeLabel(),
|
||||||
lang: translation.language,
|
lang: translation.language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "page": {
|
case Collections.Pages: {
|
||||||
const translation = getLocalizedMatch(source.page.translations);
|
const translation = getLocalizedMatch(relation.value.translations);
|
||||||
return {
|
return {
|
||||||
href: getLocalizedUrl(`/pages/${source.page.slug}`),
|
href: getLocalizedUrl(`/pages/${relation.value.slug}`),
|
||||||
typeLabel: t("global.sources.typeLabel.page"),
|
typeLabel: t("global.sources.typeLabel.page"),
|
||||||
label: formatInlineTitle(translation),
|
label: formatInlineTitle(translation),
|
||||||
lang: translation.language,
|
lang: translation.language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "folder": {
|
case Collections.Folders: {
|
||||||
const translation = getLocalizedMatch(source.folder.translations);
|
const translation = getLocalizedMatch(relation.value.translations);
|
||||||
return {
|
return {
|
||||||
href: getLocalizedUrl(`/folders/${source.folder.slug}`),
|
href: getLocalizedUrl(`/folders/${relation.value.slug}`),
|
||||||
typeLabel: t("global.sources.typeLabel.folder"),
|
typeLabel: t("global.sources.typeLabel.folder"),
|
||||||
label: formatInlineTitle(translation),
|
label: formatInlineTitle(translation),
|
||||||
lang: translation.language,
|
lang: translation.language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "scans": {
|
/* TODO: Handle other types of relations */
|
||||||
const translation = getLocalizedMatch(source.collectible.translations);
|
case Collections.Audios:
|
||||||
return {
|
case Collections.ChronologyEvents:
|
||||||
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}/scans`),
|
case Collections.Files:
|
||||||
typeLabel: t("global.sources.typeLabel.scans"),
|
case Collections.Images:
|
||||||
label: formatInlineTitle(translation),
|
case Collections.Recorders:
|
||||||
lang: translation.language,
|
case Collections.Tags:
|
||||||
};
|
case Collections.Videos:
|
||||||
}
|
default:
|
||||||
|
|
||||||
case "gallery": {
|
|
||||||
const translation = getLocalizedMatch(source.collectible.translations);
|
|
||||||
return {
|
|
||||||
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}/gallery`),
|
|
||||||
typeLabel: t("global.sources.typeLabel.gallery"),
|
|
||||||
label: formatInlineTitle(translation),
|
|
||||||
lang: translation.language,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return {
|
return {
|
||||||
href: "/404",
|
href: "/404",
|
||||||
label: `Invalid type ${source["type"]}`,
|
label: `Invalid type ${relation["type"]}`,
|
||||||
typeLabel: "Error",
|
typeLabel: "Error",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -353,7 +340,7 @@ export const getI18n = async (locale: string) => {
|
||||||
formatMillimeters,
|
formatMillimeters,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
formatTimelineDate,
|
formatTimelineDate,
|
||||||
formatEndpointSource,
|
formatEndpointRelation,
|
||||||
formatScanIndexShort,
|
formatScanIndexShort,
|
||||||
formatFilesize,
|
formatFilesize,
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const pageCachingMiddleware = defineMiddleware(async ({ url, request, loc
|
||||||
response.headers.set("Last-Modified", new Date().toUTCString());
|
response.headers.set("Last-Modified", new Date().toUTCString());
|
||||||
|
|
||||||
if (locals.pageCaching) {
|
if (locals.pageCaching) {
|
||||||
pageCache.set(pathname, response, [...locals.sdkCalls]);
|
pageCache.set(pathname, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { getCurrentLocale } from "src/middleware/utils";
|
||||||
|
|
||||||
export const setAstroLocalsMiddleware = defineMiddleware(async ({ url, locals }, next) => {
|
export const setAstroLocalsMiddleware = defineMiddleware(async ({ url, locals }, next) => {
|
||||||
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
|
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
|
||||||
locals.sdkCalls = new Set();
|
|
||||||
locals.pageCaching = true;
|
locals.pageCaching = true;
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { Collections } from "src/shared/payload/constants";
|
import { contextCache, dataCache, pageCache } from "src/services.ts";
|
||||||
import type { AfterOperationWebHookMessage } from "src/shared/payload/webhooks";
|
import type { EndpointChange } from "src/shared/payload/webhooks";
|
||||||
import { contextCache, dataCache } from "src/services.ts";
|
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request, locals }) => {
|
export const POST: APIRoute = async ({ request, locals }) => {
|
||||||
locals.pageCaching = false;
|
locals.pageCaching = false;
|
||||||
|
@ -11,38 +10,17 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||||
return new Response(null, { status: 403, statusText: "Forbidden" });
|
return new Response(null, { status: 403, statusText: "Forbidden" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = (await request.json()) as AfterOperationWebHookMessage;
|
const changes = (await request.json()) as EndpointChange[];
|
||||||
console.log("[Webhook] Received messages from CMS:", message);
|
console.log("[Webhook] Received messages from CMS:", changes);
|
||||||
|
|
||||||
// Not awaiting on purpose to respond with a 202 and not block the CMS
|
// Not awaiting on purpose to respond with a 202 and not block the CMS
|
||||||
handleWebHookMessage(message);
|
handleWebHookMessage(changes);
|
||||||
|
|
||||||
return new Response(null, { status: 202, statusText: "Accepted" });
|
return new Response(null, { status: 202, statusText: "Accepted" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWebHookMessage = async ({
|
const handleWebHookMessage = async (changes: EndpointChange[]) => {
|
||||||
addedDependantIds,
|
await dataCache.invalidate(changes);
|
||||||
collection,
|
await contextCache.invalidate(changes);
|
||||||
urls,
|
await pageCache.invalidate(changes);
|
||||||
id,
|
|
||||||
}: AfterOperationWebHookMessage) => {
|
|
||||||
await dataCache.invalidate([...(id ? [id] : []), ...addedDependantIds], urls);
|
|
||||||
|
|
||||||
switch (collection) {
|
|
||||||
case Collections.Wordings:
|
|
||||||
await contextCache.refreshWordings();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Collections.Currencies:
|
|
||||||
await contextCache.refreshCurrencies();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Collections.Languages:
|
|
||||||
await contextCache.refreshLocales();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Collections.WebsiteConfig:
|
|
||||||
await contextCache.refreshWebsiteConfig();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { dataCache, pageCache } from "src/services";
|
import { contextCache, dataCache, pageCache } from "src/services";
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ locals }) => {
|
export const GET: APIRoute = async ({ locals }) => {
|
||||||
locals.pageCaching = false;
|
locals.pageCaching = false;
|
||||||
|
|
||||||
|
await contextCache.init();
|
||||||
await dataCache.init();
|
await dataCache.init();
|
||||||
await pageCache.init();
|
await pageCache.init();
|
||||||
return new Response(null, { status: 200, statusText: "Ok" });
|
return new Response(null, { status: 200, statusText: "Ok" });
|
||||||
|
|
|
@ -32,7 +32,6 @@ const { translations, thumbnail, createdAt, updatedAt, updatedBy, attributes } =
|
||||||
await (async () => {
|
await (async () => {
|
||||||
if (Astro.props.page) return Astro.props.page;
|
if (Astro.props.page) return Astro.props.page;
|
||||||
const response = await payload.getPage(slug);
|
const response = await payload.getPage(slug);
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@ const index = Astro.props.index ?? parseInt(reqUrl.searchParams.get("index")!);
|
||||||
const event = await (async () => {
|
const event = await (async () => {
|
||||||
if (Astro.props.event) return Astro.props.event;
|
if (Astro.props.event) return Astro.props.event;
|
||||||
const response = await payload.getChronologyEventByID(id);
|
const response = await payload.getChronologyEventByID(id);
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
})();
|
})();
|
||||||
const { sources, translations } = event.events[index]!;
|
const { sources, translations } = event.events[index]!;
|
||||||
|
|
|
@ -17,7 +17,6 @@ const response = await fetchOr404(() => payload.getAudioByID(id));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
const audio = response.data;
|
const audio = response.data;
|
||||||
|
|
||||||
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
|
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
|
||||||
|
@ -34,6 +33,7 @@ const {
|
||||||
updatedAt,
|
updatedAt,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
mimeType,
|
mimeType,
|
||||||
|
backlinks,
|
||||||
} = audio;
|
} = audio;
|
||||||
|
|
||||||
const { pretitle, title, subtitle, description, language } = getLocalizedMatch(translations);
|
const { pretitle, title, subtitle, description, language } = getLocalizedMatch(translations);
|
||||||
|
@ -73,6 +73,7 @@ const metaAttributes = [
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
<AppLayout
|
<AppLayout
|
||||||
|
backlinks={backlinks}
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: formatInlineTitle({ pretitle, title, subtitle }),
|
title: formatInlineTitle({ pretitle, title, subtitle }),
|
||||||
description: description && formatRichTextToString(description),
|
description: description && formatRichTextToString(description),
|
||||||
|
|
|
@ -17,8 +17,7 @@ const response = await fetchOr404(() => payload.getCollectibleGalleryImage(slug,
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
const { backlinks, previousIndex, nextIndex, image } = response.data;
|
||||||
const { parentPages, previousIndex, nextIndex, image } = response.data;
|
|
||||||
|
|
||||||
const { filename, translations, createdAt, updatedAt, credits, attributes, mimeType } = image;
|
const { filename, translations, createdAt, updatedAt, credits, attributes, mimeType } = image;
|
||||||
|
|
||||||
|
@ -67,7 +66,7 @@ const metaAttributes = [
|
||||||
description: description && formatRichTextToString(description),
|
description: description && formatRichTextToString(description),
|
||||||
thumbnail: image,
|
thumbnail: image,
|
||||||
}}
|
}}
|
||||||
parentPages={parentPages}>
|
backlinks={backlinks}>
|
||||||
<Lightbox
|
<Lightbox
|
||||||
image={image}
|
image={image}
|
||||||
pretitle={pretitle}
|
pretitle={pretitle}
|
||||||
|
|
|
@ -15,8 +15,7 @@ const response = await fetchOr404(() => payload.getCollectibleGallery(slug));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
const { translations, backlinks, images, thumbnail } = response.data;
|
||||||
const { translations, parentPages, images, thumbnail } = response.data;
|
|
||||||
|
|
||||||
const translation = getLocalizedMatch(translations);
|
const translation = getLocalizedMatch(translations);
|
||||||
---
|
---
|
||||||
|
@ -29,7 +28,7 @@ const translation = getLocalizedMatch(translations);
|
||||||
description: translation.description && formatRichTextToString(translation.description),
|
description: translation.description && formatRichTextToString(translation.description),
|
||||||
thumbnail,
|
thumbnail,
|
||||||
}}
|
}}
|
||||||
parentPages={parentPages}
|
backlinks={backlinks}
|
||||||
class="app">
|
class="app">
|
||||||
<AppLayoutTitle
|
<AppLayoutTitle
|
||||||
title={translation.title}
|
title={translation.title}
|
||||||
|
|
|
@ -30,7 +30,6 @@ const response = await fetchOr404(() => payload.getCollectible(slug));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
const collectible = response.data;
|
const collectible = response.data;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -47,7 +46,7 @@ const {
|
||||||
scans,
|
scans,
|
||||||
subitems,
|
subitems,
|
||||||
files,
|
files,
|
||||||
parentPages,
|
backlinks,
|
||||||
attributes,
|
attributes,
|
||||||
contents,
|
contents,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
@ -128,7 +127,7 @@ if (languages.length > 0) {
|
||||||
description: description && formatRichTextToString(description),
|
description: description && formatRichTextToString(description),
|
||||||
thumbnail,
|
thumbnail,
|
||||||
}}
|
}}
|
||||||
parentPages={parentPages}
|
backlinks={backlinks}
|
||||||
backgroundImage={backgroundImage ?? thumbnail}>
|
backgroundImage={backgroundImage ?? thumbnail}>
|
||||||
<AsideLayout reducedAsideWidth>
|
<AsideLayout reducedAsideWidth>
|
||||||
<Fragment slot="header">
|
<Fragment slot="header">
|
||||||
|
|
|
@ -16,8 +16,7 @@ const response = await fetchOr404(() => payload.getCollectibleScanPage(slug, ind
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
const { backlinks, previousIndex, nextIndex, image, translations } = response.data;
|
||||||
const { parentPages, previousIndex, nextIndex, image, translations } = response.data;
|
|
||||||
|
|
||||||
const translation = getLocalizedMatch(translations);
|
const translation = getLocalizedMatch(translations);
|
||||||
---
|
---
|
||||||
|
@ -29,7 +28,7 @@ const translation = getLocalizedMatch(translations);
|
||||||
title: `${formatInlineTitle(translation)} (${index})`,
|
title: `${formatInlineTitle(translation)} (${index})`,
|
||||||
description: translation.description && formatRichTextToString(translation.description),
|
description: translation.description && formatRichTextToString(translation.description),
|
||||||
}}
|
}}
|
||||||
parentPages={parentPages}>
|
backlinks={backlinks}>
|
||||||
<Lightbox
|
<Lightbox
|
||||||
image={image}
|
image={image}
|
||||||
title={formatScanIndexShort(index)}
|
title={formatScanIndexShort(index)}
|
||||||
|
|
|
@ -16,8 +16,7 @@ const response = await fetchOr404(() => payload.getCollectibleScans(slug));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
const { translations, credits, cover, pages, dustjacket, obi, backlinks, thumbnail } =
|
||||||
const { translations, credits, cover, pages, dustjacket, obi, parentPages, thumbnail } =
|
|
||||||
response.data;
|
response.data;
|
||||||
|
|
||||||
const translation = getLocalizedMatch(translations);
|
const translation = getLocalizedMatch(translations);
|
||||||
|
@ -46,7 +45,7 @@ const hasOutsideObi = obi ? Object.keys(obi).some((value) => !value.includes("in
|
||||||
description: translation.description && formatRichTextToString(translation.description),
|
description: translation.description && formatRichTextToString(translation.description),
|
||||||
thumbnail,
|
thumbnail,
|
||||||
}}
|
}}
|
||||||
parentPages={parentPages}
|
backlinks={backlinks}
|
||||||
class="app">
|
class="app">
|
||||||
<AppLayoutTitle
|
<AppLayoutTitle
|
||||||
title={translation.title}
|
title={translation.title}
|
||||||
|
|
|
@ -18,7 +18,6 @@ const response = await fetchOr404(() => payload.getFileByID(id));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
const {
|
const {
|
||||||
translations,
|
translations,
|
||||||
attributes,
|
attributes,
|
||||||
|
@ -30,6 +29,7 @@ const {
|
||||||
updatedAt,
|
updatedAt,
|
||||||
createdAt,
|
createdAt,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
backlinks,
|
||||||
} = response.data;
|
} = response.data;
|
||||||
|
|
||||||
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
|
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
|
||||||
|
@ -84,6 +84,7 @@ const smallTitle = title === filename;
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
<AppLayout
|
<AppLayout
|
||||||
|
backlinks={backlinks}
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: formatInlineTitle({ pretitle, title, subtitle }),
|
title: formatInlineTitle({ pretitle, title, subtitle }),
|
||||||
description: description && formatRichTextToString(description),
|
description: description && formatRichTextToString(description),
|
||||||
|
|
|
@ -22,8 +22,7 @@ const response = await fetchOr404(() => payload.getFolder(slug));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
const { files, backlinks, sections, translations } = response.data;
|
||||||
const { files, parentPages, sections, translations } = response.data;
|
|
||||||
|
|
||||||
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
|
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
|
||||||
const { language, title, description } = getLocalizedMatch(translations);
|
const { language, title, description } = getLocalizedMatch(translations);
|
||||||
|
@ -36,7 +35,7 @@ const { language, title, description } = getLocalizedMatch(translations);
|
||||||
title: title,
|
title: title,
|
||||||
description: description && formatRichTextToString(description),
|
description: description && formatRichTextToString(description),
|
||||||
}}
|
}}
|
||||||
parentPages={parentPages}
|
backlinks={backlinks}
|
||||||
class="app">
|
class="app">
|
||||||
<AppLayoutTitle title={title} lang={language} />
|
<AppLayoutTitle title={title} lang={language} />
|
||||||
{description && <RichText content={description} context={{ lang: language }} />}
|
{description && <RichText content={description} context={{ lang: language }} />}
|
||||||
|
|
|
@ -12,7 +12,6 @@ const response = await fetchOr404(() => payload.getImageByID(id));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
const image = response.data;
|
const image = response.data;
|
||||||
const {
|
const {
|
||||||
filename,
|
filename,
|
||||||
|
@ -25,6 +24,7 @@ const {
|
||||||
filesize,
|
filesize,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
backlinks,
|
||||||
} = image;
|
} = image;
|
||||||
|
|
||||||
const { getLocalizedMatch, formatDate, t, formatFilesize, formatNumber } = await getI18n(
|
const { getLocalizedMatch, formatDate, t, formatFilesize, formatNumber } = await getI18n(
|
||||||
|
@ -83,6 +83,7 @@ const metaAttributes = [
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
<AppLayout
|
<AppLayout
|
||||||
|
backlinks={backlinks}
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: formatInlineTitle({ pretitle, title, subtitle }),
|
title: formatInlineTitle({ pretitle, title, subtitle }),
|
||||||
description: description && formatRichTextToString(description),
|
description: description && formatRichTextToString(description),
|
||||||
|
|
|
@ -12,9 +12,8 @@ const response = await fetchOr404(() => payload.getPage(slug));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
const page = response.data;
|
const page = response.data;
|
||||||
const { parentPages, thumbnail, translations, backgroundImage } = page;
|
const { backlinks, thumbnail, translations, backgroundImage } = page;
|
||||||
|
|
||||||
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
|
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
|
||||||
const meta = getLocalizedMatch(translations);
|
const meta = getLocalizedMatch(translations);
|
||||||
|
@ -28,7 +27,7 @@ const meta = getLocalizedMatch(translations);
|
||||||
description: meta.summary && formatRichTextToString(meta.summary),
|
description: meta.summary && formatRichTextToString(meta.summary),
|
||||||
thumbnail: thumbnail,
|
thumbnail: thumbnail,
|
||||||
}}
|
}}
|
||||||
parentPages={parentPages}
|
backlinks={backlinks}
|
||||||
backgroundImage={backgroundImage ?? thumbnail}>
|
backgroundImage={backgroundImage ?? thumbnail}>
|
||||||
<Page slug={slug} lang={Astro.locals.currentLocale} page={page} />
|
<Page slug={slug} lang={Astro.locals.currentLocale} page={page} />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|
|
@ -16,7 +16,6 @@ const response = await fetchOr404(() => payload.getRecorderByID(id));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
const { username, languages, avatar, translations } = response.data;
|
const { username, languages, avatar, translations } = response.data;
|
||||||
|
|
||||||
const { t, getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
const { t, getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
---
|
---
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import SourceRow from "components/SourceRow.astro";
|
import RelationRow from "components/RelationRow.astro";
|
||||||
import Tooltip from "components/Tooltip.astro";
|
import Tooltip from "components/Tooltip.astro";
|
||||||
import { getI18n } from "src/i18n/i18n";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import type { EndpointSource } from "src/shared/payload/endpoint-types";
|
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sources: EndpointSource[];
|
sources: EndpointRelation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sources } = Astro.props;
|
const { sources } = Astro.props;
|
||||||
|
@ -26,7 +26,7 @@ const { t } = await getI18n(Astro.locals.currentLocale);
|
||||||
<Tooltip trigger="click">
|
<Tooltip trigger="click">
|
||||||
<div id="tooltip-content" slot="tooltip-content">
|
<div id="tooltip-content" slot="tooltip-content">
|
||||||
{sources.map((source) => (
|
{sources.map((source) => (
|
||||||
<SourceRow source={source} />
|
<RelationRow relation={source} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button class="pressable-label">
|
<button class="pressable-label">
|
||||||
|
|
|
@ -14,7 +14,6 @@ const response = await fetchOr404(() => payload.getChronologyEvents());
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
const events = response.data;
|
const events = response.data;
|
||||||
|
|
||||||
const groupedEvents = groupBy(events, (event) => event.date.year);
|
const groupedEvents = groupBy(events, (event) => event.date.year);
|
||||||
|
|
|
@ -17,7 +17,6 @@ const response = await fetchOr404(() => payload.getVideoByID(id));
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
Astro.locals.sdkCalls.add(response.endpointCalled);
|
|
||||||
const video = response.data;
|
const video = response.data;
|
||||||
const {
|
const {
|
||||||
translations,
|
translations,
|
||||||
|
@ -30,6 +29,7 @@ const {
|
||||||
createdAt,
|
createdAt,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
mimeType,
|
mimeType,
|
||||||
|
backlinks,
|
||||||
} = video;
|
} = video;
|
||||||
|
|
||||||
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
|
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
|
||||||
|
@ -73,6 +73,7 @@ const metaAttributes = [
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
||||||
<AppLayout
|
<AppLayout
|
||||||
|
backlinks={backlinks}
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: formatInlineTitle({ pretitle, title, subtitle }),
|
title: formatInlineTitle({ pretitle, title, subtitle }),
|
||||||
description: description && formatRichTextToString(description),
|
description: description && formatRichTextToString(description),
|
||||||
|
|
|
@ -22,6 +22,13 @@ export const analytics = import.meta.env.ANALYTICS_URL
|
||||||
|
|
||||||
const tokenCache = new TokenCache();
|
const tokenCache = new TokenCache();
|
||||||
|
|
||||||
|
const uncachedPayload = new PayloadSDK(
|
||||||
|
import.meta.env.PAYLOAD_API_URL,
|
||||||
|
import.meta.env.PAYLOAD_USER,
|
||||||
|
import.meta.env.PAYLOAD_PASSWORD
|
||||||
|
);
|
||||||
|
uncachedPayload.addTokenCache(tokenCache);
|
||||||
|
|
||||||
export const payload = new PayloadSDK(
|
export const payload = new PayloadSDK(
|
||||||
import.meta.env.PAYLOAD_API_URL,
|
import.meta.env.PAYLOAD_API_URL,
|
||||||
import.meta.env.PAYLOAD_USER,
|
import.meta.env.PAYLOAD_USER,
|
||||||
|
@ -29,19 +36,11 @@ export const payload = new PayloadSDK(
|
||||||
);
|
);
|
||||||
payload.addTokenCache(tokenCache);
|
payload.addTokenCache(tokenCache);
|
||||||
|
|
||||||
const uncachedPayload = new PayloadSDK(
|
|
||||||
import.meta.env.PAYLOAD_API_URL,
|
|
||||||
import.meta.env.PAYLOAD_USER,
|
|
||||||
import.meta.env.PAYLOAD_PASSWORD
|
|
||||||
);
|
|
||||||
uncachedPayload.addTokenCache(tokenCache);
|
|
||||||
export const pageCache = new PageCache(uncachedPayload);
|
|
||||||
|
|
||||||
// Loading context cache first so that the server can still serve responses while precaching.
|
// Loading context cache first so that the server can still serve responses while precaching.
|
||||||
export const contextCache = new ContextCache(payload);
|
export const contextCache = new ContextCache(payload);
|
||||||
await contextCache.init();
|
await contextCache.init();
|
||||||
|
|
||||||
export const dataCache = new DataCache(payload, uncachedPayload, (urls) =>
|
export const dataCache = new DataCache(payload, uncachedPayload);
|
||||||
pageCache.invalidate(urls)
|
|
||||||
);
|
|
||||||
payload.addDataCache(dataCache);
|
payload.addDataCache(dataCache);
|
||||||
|
|
||||||
|
export const pageCache = new PageCache(uncachedPayload, contextCache);
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 47c990080173a2330f0c7a9837dac34dba4e0811
|
Subproject commit caa79dee9eca5b9b6959e6f5a721245202423612
|
Loading…
Reference in New Issue