feat: Add the /admin pages
This commit is contained in:
parent
ae68e5a5cb
commit
3ebeb8ed6a
9 changed files with 218 additions and 9 deletions
58
src/routes/admin/+layout.svelte
Normal file
58
src/routes/admin/+layout.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from "$app/state";
|
||||||
|
|
||||||
|
const { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="admin-layout">
|
||||||
|
{#if page.url.pathname !== "/admin/login"}
|
||||||
|
<nav class="navbar">
|
||||||
|
<menu class="left">
|
||||||
|
<li><a href="/admin/photos">Photos</a></li>
|
||||||
|
<li><a href="/admin/photos/upload">Upload a Photo</a></li>
|
||||||
|
<li><a href="/blog/new">Write a new blog post</a></li>
|
||||||
|
</menu>
|
||||||
|
<menu class="right">
|
||||||
|
<li>
|
||||||
|
<a href="/admin/logout">Log Out</a>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.admin-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: min-content auto;
|
||||||
|
min-height: 100dvh;
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
width: 100dvw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar menu li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a::after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
<h1>Welcome home, Wilson</h1>
|
<h1>Welcome home, Wilson</h1>
|
||||||
|
|
||||||
|
<a href="/admin/photos">Upload Photo</a>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,39 @@
|
||||||
<h1>Admin login</h1>
|
<h1>Admin login</h1>
|
||||||
|
|
||||||
<form method="POST">
|
<div class="container">
|
||||||
<div class="field">
|
<form method="POST" class="form">
|
||||||
<label for="token">API Token</label>
|
<div class="field">
|
||||||
<input type="password" id="token" name="token" />
|
<label for="token">Password</label>
|
||||||
</div>
|
<input type="password" id="token" name="token" />
|
||||||
<div class="field">
|
</div>
|
||||||
<input type="submit" value="Login">
|
<div class="field">
|
||||||
</div>
|
<input type="submit" value="Login" class="thomaswilson-button" />
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
width: 560px;
|
||||||
|
max-width: 90dvw;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field label {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
7
src/routes/admin/logout/+server.ts
Normal file
7
src/routes/admin/logout/+server.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
|
||||||
|
import { redirect, type ServerLoad } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const GET: ServerLoad = ({ cookies }) => {
|
||||||
|
new CookieAuthentication(cookies).logout();
|
||||||
|
redirect(307, "/");
|
||||||
|
};
|
||||||
47
src/routes/admin/photos/+page.server.ts
Normal file
47
src/routes/admin/photos/+page.server.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { writeFile } from "node:fs/promises";
|
||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { PRIVATE_PHOTO_UPLOAD_DIR } from "$env/static/private";
|
||||||
|
import type { Actions, ServerLoad } from "@sveltejs/kit";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
export const load: ServerLoad = async ({ locals }) => {
|
||||||
|
const photos = await locals.prisma.photoPost.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
fileName: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { photos };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ request, locals }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const file = formData.get("file") as File;
|
||||||
|
const title = formData.get("title") as string;
|
||||||
|
const description = formData.get("description") as string;
|
||||||
|
|
||||||
|
const filetype = file.type.split("/")[1];
|
||||||
|
const fileName = `${randomUUID()}.${filetype}`;
|
||||||
|
const fileLocation = join(PRIVATE_PHOTO_UPLOAD_DIR, fileName);
|
||||||
|
|
||||||
|
const fileContentBuffer = await file.arrayBuffer();
|
||||||
|
await writeFile(fileLocation, Buffer.from(fileContentBuffer));
|
||||||
|
|
||||||
|
await locals.prisma.photoPost.create({
|
||||||
|
data: {
|
||||||
|
fileName,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
} satisfies Actions;
|
||||||
12
src/routes/admin/photos/+page.svelte
Normal file
12
src/routes/admin/photos/+page.svelte
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import PhotoPostForm from "../../../components/admin/PhotoPostForm.svelte";
|
||||||
|
import type { PageProps } from "./$types.js";
|
||||||
|
import PhotoFeed from "./PhotoFeed.svelte";
|
||||||
|
|
||||||
|
const { data }: PageProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1>Photo Posts</h1>
|
||||||
|
<PhotoFeed photos={data.photos} />
|
||||||
|
</div>
|
||||||
0
src/routes/admin/photos/FeedItem.svelte
Normal file
0
src/routes/admin/photos/FeedItem.svelte
Normal file
35
src/routes/admin/photos/PhotoFeed.svelte
Normal file
35
src/routes/admin/photos/PhotoFeed.svelte
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PhotoPost } from "../../../../generated/prisma/client.js";
|
||||||
|
|
||||||
|
const { photos }: { photos: PhotoPost[] } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="photo-feed">
|
||||||
|
{#each photos as photo, id}
|
||||||
|
<li class="item">
|
||||||
|
<img width="250" src={`/image/${photo.fileName}`} alt={photo.title} />
|
||||||
|
<p>{photo.title}</p>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.photo-feed {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item img {
|
||||||
|
width: 250px;
|
||||||
|
height: auto;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
src/routes/admin/photos/upload/+page.svelte
Normal file
20
src/routes/admin/photos/upload/+page.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script>
|
||||||
|
import PhotoPostForm from "../../../../components/admin/PhotoPostForm.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-container">
|
||||||
|
<h1>Upload a Photo</h1>
|
||||||
|
<PhotoPostForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue