giga refactor of the API calls
This commit is contained in:
parent
526763b795
commit
b6d9d45398
29 changed files with 743 additions and 933 deletions
1099
package-lock.json
generated
1099
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
@ -2,6 +2,9 @@
|
||||||
"name": "dipper",
|
"name": "dipper",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
@ -16,17 +19,17 @@
|
||||||
"@sveltejs/kit": "^1.20.4",
|
"@sveltejs/kit": "^1.20.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte": "^2.30.0",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
"svelte": "^4.0.0",
|
"svelte": "^4.0.0",
|
||||||
"svelte-check": "^3.4.3",
|
"svelte-check": "^3.4.3",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.1.6",
|
||||||
"vite": "^4.3.6",
|
"vite": "^4.4.0",
|
||||||
"sveltekit-adapter-deno": "^0.10.1"
|
"sveltekit-adapter-deno": "^0.10.2"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
|
44
src/lib/api/auth.ts
Normal file
44
src/lib/api/auth.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import type { Cookies } from '@sveltejs/kit';
|
||||||
|
import { callJSON, callText } from './request';
|
||||||
|
|
||||||
|
interface LoginParameters {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginResponse {
|
||||||
|
expires_at: string;
|
||||||
|
session_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OkResponse {
|
||||||
|
ok: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const login = (cookies: Cookies, username: string, password: string) =>
|
||||||
|
callJSON<LoginResponse, LoginParameters>(
|
||||||
|
'POST',
|
||||||
|
'auth/login',
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
},
|
||||||
|
cookies
|
||||||
|
);
|
||||||
|
|
||||||
|
export const logout = (cookies: Cookies) =>
|
||||||
|
callJSON<OkResponse>('POST', 'auth/logout', undefined, cookies);
|
||||||
|
export const getLoggedInUser = (cookies: Cookies) => callText('GET', 'auth/me', undefined, cookies);
|
||||||
|
|
||||||
|
export async function isLoggedIn(cookies: Cookies) {
|
||||||
|
const session = cookies.get('session');
|
||||||
|
if (session) {
|
||||||
|
try {
|
||||||
|
// Check that login is valid
|
||||||
|
await getLoggedInUser(cookies);
|
||||||
|
return true;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
21
src/lib/api/collections.ts
Normal file
21
src/lib/api/collections.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Cookies } from '@sveltejs/kit';
|
||||||
|
import type { PaginatedWithCursor, TimePaginationQuery } from './pagination';
|
||||||
|
import type { Post } from './posts';
|
||||||
|
import { callJSON, withQuery } from './request';
|
||||||
|
|
||||||
|
export interface Collection {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
parent: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCollectionPosts = (
|
||||||
|
name: string,
|
||||||
|
collection: string,
|
||||||
|
pagination?: TimePaginationQuery
|
||||||
|
) =>
|
||||||
|
callJSON<PaginatedWithCursor<Post, string>>(
|
||||||
|
'GET',
|
||||||
|
withQuery(`collections/${name}/${collection}/posts`, pagination)
|
||||||
|
);
|
82
src/lib/api/cookies.ts
Normal file
82
src/lib/api/cookies.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { PUBLIC_COOKIES_SECURE, PUBLIC_DOMAIN } from '$env/static/public';
|
||||||
|
import { parseBool } from '$lib/env';
|
||||||
|
import type { Cookies } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
interface CookieInfo {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
options: {
|
||||||
|
SameSite?: 'Strict' | 'Lax' | 'None';
|
||||||
|
Secure: boolean;
|
||||||
|
Partitioned: boolean;
|
||||||
|
HttpOnly: boolean;
|
||||||
|
Expires?: Date;
|
||||||
|
'Max-Age'?: number;
|
||||||
|
Path?: string;
|
||||||
|
Domain?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCookieHeader(headerValue: string): CookieInfo {
|
||||||
|
const [nv, ...options] = headerValue.split(';');
|
||||||
|
const [name, value] = nv.split('=').map((v) => v.trim());
|
||||||
|
const cookie: CookieInfo = {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
options: {
|
||||||
|
Secure: false,
|
||||||
|
Partitioned: false,
|
||||||
|
HttpOnly: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
options.forEach((option) => {
|
||||||
|
const [key, value] = option.split('=').map((v) => v.trim());
|
||||||
|
switch (key) {
|
||||||
|
case 'Secure':
|
||||||
|
cookie.options.Secure = true;
|
||||||
|
break;
|
||||||
|
case 'SameSite':
|
||||||
|
cookie.options.SameSite = value as 'Strict' | 'Lax' | 'None';
|
||||||
|
break;
|
||||||
|
case 'Partitioned':
|
||||||
|
cookie.options.Partitioned = true;
|
||||||
|
break;
|
||||||
|
case 'HttpOnly':
|
||||||
|
cookie.options.HttpOnly = true;
|
||||||
|
break;
|
||||||
|
case 'Expires':
|
||||||
|
cookie.options.Expires = new Date(value);
|
||||||
|
break;
|
||||||
|
case 'Max-Age':
|
||||||
|
cookie.options['Max-Age'] = parseInt(value, 10);
|
||||||
|
break;
|
||||||
|
case 'Path':
|
||||||
|
cookie.options.Path = value;
|
||||||
|
break;
|
||||||
|
case 'Domain':
|
||||||
|
cookie.options.Domain = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function passthroughSession(response: Response, cookies: Cookies) {
|
||||||
|
if (response.ok) {
|
||||||
|
const cookieHeader = response.headers.get('Set-Cookie');
|
||||||
|
if (!cookieHeader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const info = parseCookieHeader(cookieHeader);
|
||||||
|
cookies.set(info.name, info.value, {
|
||||||
|
httpOnly: info.options.HttpOnly,
|
||||||
|
secure: parseBool(PUBLIC_COOKIES_SECURE),
|
||||||
|
sameSite: 'strict',
|
||||||
|
domain: PUBLIC_DOMAIN,
|
||||||
|
expires: info.options.Expires,
|
||||||
|
maxAge: info.options['Max-Age']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
11
src/lib/api/pagination.ts
Normal file
11
src/lib/api/pagination.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { QuerySerializable } from './request';
|
||||||
|
|
||||||
|
export interface PaginatedWithCursor<T, C> {
|
||||||
|
items: T[];
|
||||||
|
next_cursor: C | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimePaginationQuery extends QuerySerializable {
|
||||||
|
limit: number;
|
||||||
|
before: Date | null;
|
||||||
|
}
|
19
src/lib/api/posts.ts
Normal file
19
src/lib/api/posts.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { callJSON } from './request';
|
||||||
|
|
||||||
|
export interface Post {
|
||||||
|
id: string;
|
||||||
|
site: string;
|
||||||
|
author: string;
|
||||||
|
author_display_name: string;
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
tags: string[];
|
||||||
|
blocks: [];
|
||||||
|
created_at: string;
|
||||||
|
modified_at: string | null;
|
||||||
|
deleted_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPost = async (site: string, slug: string) =>
|
||||||
|
callJSON<Post>('GET', `posts/${site}/${slug}`);
|
91
src/lib/api/request.ts
Normal file
91
src/lib/api/request.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { PUBLIC_API_BASE } from '$env/static/public';
|
||||||
|
import type { Cookies } from '@sveltejs/kit';
|
||||||
|
import { passthroughSession } from './cookies';
|
||||||
|
|
||||||
|
export class APIError extends Error {
|
||||||
|
readonly code?: string;
|
||||||
|
|
||||||
|
constructor(readonly response: Response, body?: { code: string; message: string }) {
|
||||||
|
super(body?.message ?? response.statusText);
|
||||||
|
this.code = body?.code;
|
||||||
|
Object.setPrototypeOf(this, APIError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Stringer = { toString(): string };
|
||||||
|
export type QuerySerializable = { [key: string | number | symbol]: Stringer | null | undefined };
|
||||||
|
|
||||||
|
export function withQuery(path: string, query?: QuerySerializable) {
|
||||||
|
return query ? `${path}?${asQueryParams(query).toString()}` : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asQueryParams<T extends QuerySerializable>(obj: T) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const key of Object.getOwnPropertyNames(obj)) {
|
||||||
|
const value = obj[key];
|
||||||
|
if (value === null || value === undefined || typeof value === 'undefined') continue;
|
||||||
|
params.append(key, value.toString());
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Body = { [key: string]: unknown } | object | void;
|
||||||
|
export async function apiCall<B extends Body>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: B,
|
||||||
|
cookies?: Cookies
|
||||||
|
) {
|
||||||
|
const session = cookies?.get('session');
|
||||||
|
const response = await fetch(`${PUBLIC_API_BASE}/${path}`, {
|
||||||
|
method,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Cookie: session ? `session=${session}` : ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (cookies) {
|
||||||
|
passthroughSession(response, cookies);
|
||||||
|
}
|
||||||
|
if (response.ok) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for content
|
||||||
|
if (response.headers.get('Content-Type')?.includes('application/json')) {
|
||||||
|
const body = await response.json();
|
||||||
|
throw new APIError(response, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function callJSON<R, B extends Body = void>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: B,
|
||||||
|
cookies?: Cookies
|
||||||
|
) {
|
||||||
|
return apiCall(method, path, body, cookies).then((response) => response.json() as R);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function callText<B extends Body = void>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: B,
|
||||||
|
cookies?: Cookies
|
||||||
|
) {
|
||||||
|
return apiCall(method, path, body, cookies).then((response) => response.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function callBytes<B extends Body = void>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: B,
|
||||||
|
cookies?: Cookies
|
||||||
|
) {
|
||||||
|
return apiCall(method, path, body, cookies).then((response) => response.blob());
|
||||||
|
}
|
17
src/lib/api/sites.ts
Normal file
17
src/lib/api/sites.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import type { Collection } from './collections';
|
||||||
|
import { callJSON } from './request';
|
||||||
|
|
||||||
|
export interface Site {
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
owner_display_name: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
collections: Collection[];
|
||||||
|
default_collection: string;
|
||||||
|
created_at: string;
|
||||||
|
modified_at: string | null;
|
||||||
|
deleted_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSiteInfo = (name: string) => callJSON<Site>('GET', `sites/${name}`);
|
16
src/lib/env.ts
Normal file
16
src/lib/env.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export function parseBool(str: string, defaultValue: boolean = false): boolean {
|
||||||
|
switch (str.toLowerCase().trim()) {
|
||||||
|
case 'true':
|
||||||
|
case 'yes':
|
||||||
|
case 'on':
|
||||||
|
case '1':
|
||||||
|
return true;
|
||||||
|
case 'false':
|
||||||
|
case 'no':
|
||||||
|
case 'off':
|
||||||
|
case '0':
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
26
src/lib/errors.ts
Normal file
26
src/lib/errors.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { APIError } from './api/request';
|
||||||
|
|
||||||
|
/// Maps an error from the backend to a SvelteKit error.
|
||||||
|
export function mapBackendError(e: unknown) {
|
||||||
|
if (e instanceof APIError) {
|
||||||
|
switch (e.response.status) {
|
||||||
|
case 404:
|
||||||
|
return error(404, 'Not Found');
|
||||||
|
default:
|
||||||
|
// TODO handle better
|
||||||
|
return error(500, e.response.statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error(502, 'Platform is down');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures that a promise resolves to a value, or throws the appropriate error otherwise.
|
||||||
|
export async function must<T>(promise: Promise<T>): Promise<T> {
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} catch (err) {
|
||||||
|
throw mapBackendError(err);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +0,0 @@
|
||||||
export interface Site {
|
|
||||||
name: string;
|
|
||||||
owner: string;
|
|
||||||
owner_display_name: string;
|
|
||||||
title: string;
|
|
||||||
description: string | null;
|
|
||||||
collections: Collection[];
|
|
||||||
default_collection: string;
|
|
||||||
created_at: string;
|
|
||||||
modified_at: string | null;
|
|
||||||
deleted_at: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Collection {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
parent: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Post {
|
|
||||||
id: string;
|
|
||||||
site: string;
|
|
||||||
author: string;
|
|
||||||
author_display_name: string;
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
description: string | null;
|
|
||||||
tags: string[];
|
|
||||||
blocks: [];
|
|
||||||
created_at: string;
|
|
||||||
modified_at: string | null;
|
|
||||||
deleted_at: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginatedWithCursor<T, C> {
|
|
||||||
items: T[];
|
|
||||||
next_cursor: C | null;
|
|
||||||
}
|
|
|
@ -3,3 +3,10 @@ import { PUBLIC_DOMAIN } from '$env/static/public';
|
||||||
export function getSiteName(origin: string) {
|
export function getSiteName(origin: string) {
|
||||||
return origin.substring(0, origin.indexOf(PUBLIC_DOMAIN)).replaceAll('.', '');
|
return origin.substring(0, origin.indexOf(PUBLIC_DOMAIN)).replaceAll('.', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLoginURL(base: URL) {
|
||||||
|
const copy = new URL(base);
|
||||||
|
copy.hostname = PUBLIC_DOMAIN;
|
||||||
|
copy.pathname = '/login';
|
||||||
|
return copy.toString();
|
||||||
|
}
|
||||||
|
|
3
src/routes/(admin-panel)/+layout.svelte
Normal file
3
src/routes/(admin-panel)/+layout.svelte
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<h2>cenere!!!</h2>
|
||||||
|
|
||||||
|
<slot />
|
9
src/routes/(admin-panel)/admin/+layout.server.ts
Normal file
9
src/routes/(admin-panel)/admin/+layout.server.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
import { isLoggedIn } from '$lib/api/auth';
|
||||||
|
|
||||||
|
export const load = (async ({ cookies }) => {
|
||||||
|
if (!(await isLoggedIn(cookies))) {
|
||||||
|
throw redirect(302, '/login');
|
||||||
|
}
|
||||||
|
}) satisfies LayoutServerLoad;
|
5
src/routes/(admin-panel)/admin/+layout.svelte
Normal file
5
src/routes/(admin-panel)/admin/+layout.svelte
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<form method="POST" action="/login?/logout">
|
||||||
|
<button type="submit">logout</button>
|
||||||
|
</form>
|
1
src/routes/(admin-panel)/admin/+page.svelte
Normal file
1
src/routes/(admin-panel)/admin/+page.svelte
Normal file
|
@ -0,0 +1 @@
|
||||||
|
aaaa
|
47
src/routes/(admin-panel)/login/+page.server.ts
Normal file
47
src/routes/(admin-panel)/login/+page.server.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { PUBLIC_API_BASE } from '$env/static/public';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
import { APIError } from '$lib/api/request';
|
||||||
|
import { getLoggedInUser, login, logout } from '$lib/api/auth';
|
||||||
|
|
||||||
|
export const load = async function ({ url, cookies }) {
|
||||||
|
const session = cookies.get('session');
|
||||||
|
|
||||||
|
// Check that login is valid
|
||||||
|
if (session) {
|
||||||
|
let user = null;
|
||||||
|
try {
|
||||||
|
user = await getLoggedInUser(cookies);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
throw redirect(302, url.searchParams.get('then') || '/admin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} satisfies PageServerLoad;
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
login: async ({ cookies, request, url }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const username = data.get('username')?.toString();
|
||||||
|
const password = data.get('password')?.toString();
|
||||||
|
if (!username || !password) return { error: 'missing credentials' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(cookies, username, password);
|
||||||
|
throw redirect(302, url.searchParams.get('then') || '/admin');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof APIError) {
|
||||||
|
switch (e.code) {
|
||||||
|
case 'invalid-login':
|
||||||
|
return { error: 'invalid credentials' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { error: 'unknown internal error' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logout: async ({ url, cookies }) => {
|
||||||
|
await logout(cookies);
|
||||||
|
throw redirect(302, url.searchParams.get('then') || '/login');
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
15
src/routes/(admin-panel)/login/+page.svelte
Normal file
15
src/routes/(admin-panel)/login/+page.svelte
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ActionData } from './$types';
|
||||||
|
|
||||||
|
export let form: ActionData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if form}
|
||||||
|
OH NOES: {form.error}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form method="POST" action="/login?/login">
|
||||||
|
<input name="username" placeholder="user id" />
|
||||||
|
<input name="password" type="password" placeholder="password" />
|
||||||
|
<button type="submit">sign in</button>
|
||||||
|
</form>
|
11
src/routes/(user-website)/+layout.svelte
Normal file
11
src/routes/(user-website)/+layout.svelte
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { LayoutData } from './$types';
|
||||||
|
|
||||||
|
export let data: LayoutData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header><h2><a href="/">{data.site.title}</a></h2></header>
|
||||||
|
<slot />
|
||||||
|
<footer>
|
||||||
|
<a href={data.links.login}>login</a>
|
||||||
|
</footer>
|
22
src/routes/(user-website)/+layout.ts
Normal file
22
src/routes/(user-website)/+layout.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { PUBLIC_WEBSITE_URL } from '$env/static/public';
|
||||||
|
import { getLoginURL, getSiteName } from '$lib/url';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { LayoutLoad } from './$types';
|
||||||
|
import { getSiteInfo } from '$lib/api/sites';
|
||||||
|
import { must } from '$lib/errors';
|
||||||
|
|
||||||
|
export const load = (async ({ url }) => {
|
||||||
|
const site = getSiteName(url.hostname);
|
||||||
|
if (!site) {
|
||||||
|
throw redirect(302, PUBLIC_WEBSITE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteData = await must(getSiteInfo(site));
|
||||||
|
|
||||||
|
return {
|
||||||
|
site: siteData,
|
||||||
|
links: {
|
||||||
|
login: getLoginURL(url)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}) satisfies LayoutLoad;
|
|
@ -8,7 +8,7 @@
|
||||||
{#each data.pages.items as post}
|
{#each data.pages.items as post}
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<h2>{post.title}</h2>
|
<h2><a href="/p/{post.slug}">{post.title}</a></h2>
|
||||||
<h4>by {post.author_display_name} on {new Date(post.created_at).toLocaleDateString()}</h4>
|
<h4>by {post.author_display_name} on {new Date(post.created_at).toLocaleDateString()}</h4>
|
||||||
</header>
|
</header>
|
||||||
</article>
|
</article>
|
11
src/routes/(user-website)/+page.ts
Normal file
11
src/routes/(user-website)/+page.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { getCollectionPosts } from '$lib/api/collections';
|
||||||
|
import { must } from '$lib/errors';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ parent }) => {
|
||||||
|
const { site } = await parent();
|
||||||
|
|
||||||
|
return {
|
||||||
|
pages: await must(getCollectionPosts(site.name, site.default_collection))
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
11
src/routes/(user-website)/p/[slug]/+page.ts
Normal file
11
src/routes/(user-website)/p/[slug]/+page.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
import { getPost } from '$lib/api/posts';
|
||||||
|
import { must } from '$lib/errors';
|
||||||
|
|
||||||
|
export const load = (async ({ params, parent }) => {
|
||||||
|
const { site } = await parent();
|
||||||
|
|
||||||
|
return {
|
||||||
|
page: await must(getPost(site.name, params.slug))
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
|
@ -1,8 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { LayoutData } from './$types';
|
|
||||||
|
|
||||||
export let data: LayoutData;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<header><h2>{data.site.title}</h2></header>
|
|
||||||
<slot />
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { PUBLIC_API_BASE, PUBLIC_WEBSITE_URL } from '$env/static/public';
|
|
||||||
import type { Site } from '$lib/mabel-types';
|
|
||||||
import { getSiteName } from '$lib/url';
|
|
||||||
import { error, redirect } from '@sveltejs/kit';
|
|
||||||
import type { LayoutLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async ({ url }) => {
|
|
||||||
const site = getSiteName(url.hostname);
|
|
||||||
if (!site) throw redirect(301, PUBLIC_WEBSITE_URL);
|
|
||||||
|
|
||||||
const siteData = await fetch(`${PUBLIC_API_BASE}/sites/${site}`);
|
|
||||||
if (siteData.status === 404) throw error(404, 'Not Found');
|
|
||||||
|
|
||||||
return {
|
|
||||||
site: (await siteData.json()) as Site
|
|
||||||
};
|
|
||||||
}) satisfies LayoutLoad;
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { PUBLIC_API_BASE } from '$env/static/public';
|
|
||||||
import type { PaginatedWithCursor, Post } from '$lib/mabel-types';
|
|
||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async ({ parent }) => {
|
|
||||||
const { site } = await parent();
|
|
||||||
|
|
||||||
const data = await fetch(
|
|
||||||
`${PUBLIC_API_BASE}/collections/${site.name}/${site.default_collection}/posts`
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
pages: (await data.json()) as PaginatedWithCursor<Post, string>
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
import type { PageLoad } from './$types';
|
|
||||||
import { PUBLIC_API_BASE } from '$env/static/public';
|
|
||||||
import type { Post } from '$lib/mabel-types';
|
|
||||||
|
|
||||||
export const load = (async ({ params, parent }) => {
|
|
||||||
const { site } = await parent();
|
|
||||||
const pageData = await fetch(`${PUBLIC_API_BASE}/posts/${site.name}/${params.slug}`);
|
|
||||||
if (pageData.status === 404) throw error(404, 'Not Found');
|
|
||||||
|
|
||||||
return {
|
|
||||||
page: (await pageData.json()) as Post
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
Loading…
Reference in a new issue