Merge branch 'feature'

This commit is contained in:
Christian Beutel
2024-04-03 23:42:41 +02:00
16 changed files with 707 additions and 46 deletions

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import type { Comment } from "$lib/models/comment";
import { getFileURL } from "$lib/util/file_util";
import { createEventDispatcher } from "svelte";
import TextField from "../base/text_field.svelte";
import { fade } from "svelte/transition";
export let comment: Comment;
export let mode: "show" | "edit" = "show";
const dispatch = createEventDispatcher();
let editing: boolean = false;
let editedComment = comment.text;
const avatarSrc =
comment.expand && comment.expand.author
? getFileURL(comment.expand?.author, comment.expand?.author.avatar)
: "https://api.dicebear.com/7.x/initials/svg?seed=comment&backgroundType=gradientLinear";
function deleteComment() {
dispatch("delete", comment);
}
function toggleEdit() {
if (editing) {
dispatch("edit", { comment: comment, text: editedComment });
}
editing = !editing;
}
</script>
<div class="flex gap-4 items-center" in:fade={{duration: 150}} out:fade={{duration: 150}}>
<img class="rounded-full w-10 aspect-square" src={avatarSrc} alt="avatar" />
<div>
<div class="flex items-center">
<p class="text-sm font-semibold">
{comment.expand?.author.username}
</p>
{#if mode == "edit"}
<button
type="button"
class="btn-icon ml-2"
style="font-size: 0.75rem;"
on:click={toggleEdit}
><i class="fa fa-{editing ? 'check' : 'pen'}"></i></button
>
<button
type="button"
class="btn-icon text-xs"
style="font-size: 0.75rem;"
on:click={deleteComment}><i class="fa fa-trash"></i></button
>
{/if}
</div>
{#if editing}
<TextField extraClasses="mt-2" bind:value={editedComment}
></TextField>
{:else}
<p>{comment.text}</p>
{/if}
</div>
</div>

View File

@@ -1,10 +1,17 @@
<script lang="ts">
import { goto } from "$app/navigation";
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
import Tabs from "$lib/components/base/tabs.svelte";
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
import TrailDropdown from "$lib/components/trail/trail_dropdown.svelte";
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
import { Comment } from "$lib/models/comment";
import type { Trail } from "$lib/models/trail";
import {
comments_create,
comments_delete,
comments_update,
} from "$lib/stores/comment_store";
import { currentUser } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util";
import {
@@ -20,6 +27,9 @@
import "photoswipe/style.css";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
import Button from "../base/button.svelte";
import Textarea from "../base/textarea.svelte";
import CommentCard from "../comment/comment_card.svelte";
import PhotoGallery from "../photo_gallery.svelte";
export let trail: Trail;
@@ -31,6 +41,7 @@
$_("waypoints"),
$_("photos"),
$_("summit-book"),
$_("comments"),
];
let map: Map;
@@ -43,6 +54,10 @@
let openGallery: (idx?: number) => void;
let newComment: Comment = new Comment("", 0, "", trail.id ?? "");
let commentCreateLoading: boolean = false;
let commentDeleteLoading: boolean = false;
onMount(async () => {
if (mode == "overview") {
const L = (await import("leaflet")).default;
@@ -98,6 +113,42 @@
async function toggleMapFullScreen() {
goto(`/map/trail/${trail.id!}`);
}
async function createComment() {
if (!$currentUser || !trail.id) {
return;
}
commentCreateLoading = true;
newComment.author = $currentUser.id;
newComment.trail = trail.id;
comments_create(newComment).then((c) => {
newComment.text = "";
c.expand = {
author: $currentUser!,
};
trail.expand.comments_via_trail = [
c,
...(trail.expand.comments_via_trail ?? []),
];
});
commentCreateLoading = false;
}
async function editComment(data: { comment: Comment; text: string }) {
data.comment.text = data.text;
await comments_update(data.comment);
}
async function deleteComment(comment: Comment) {
commentDeleteLoading = true;
await comments_delete(comment);
trail.expand.comments_via_trail =
trail.expand.comments_via_trail.filter((c) => c.id !== comment.id);
commentDeleteLoading = false;
}
</script>
<div
@@ -228,6 +279,53 @@
{/each}
</ul>
{/if}
{#if activeTab == 4}
<div>
{#if $currentUser}
<div class="flex items-center gap-4">
<img
class="rounded-full w-10 aspect-square"
src={getFileURL(
$currentUser,
$currentUser.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
<div class="basis-full">
<Textarea
bind:value={newComment.text}
rows={2}
placeholder="Add comment..."
></Textarea>
</div>
</div>
<div class="flex justify-end mt-3">
<Button
on:click={createComment}
loading={commentCreateLoading}
secondary={true}
disabled={newComment.text.length == 0}
>Comment</Button
>
</div>
{/if}
<ul>
{#each trail.expand.comments_via_trail ?? [] as comment}
<li>
<CommentCard
{comment}
mode={comment.author == $currentUser?.id
? "edit"
: "show"}
on:delete={(e) => deleteComment(e.detail)}
on:edit={(e) => editComment(e.detail)}
></CommentCard>
</li>
{/each}
</ul>
</div>
{/if}
{#if mode == "overview"}
<div class="relative">
<div class="rounded-xl h-72" id="map">

View File

@@ -18,6 +18,7 @@
"change": "更换",
"changelog": "变更日志",
"chinese": "华人",
"comments": "",
"completed": "已完成",
"completion-status": "完成状态",
"contribute": "贡献",

View File

@@ -0,0 +1,19 @@
import type { User } from "$lib/stores/user_store";
export class Comment {
id?: string;
text: string;
rating: number;
author: string;
trail: string;
expand?: {
author: User
}
constructor(text: string, rating: number, author: string, trail: string) {
this.text = text;
this.rating = rating;
this.author = author;
this.trail = trail;
}
}

View File

@@ -1,4 +1,5 @@
import type { Category } from "./category";
import type { Comment } from "./comment";
import type { SummitLog } from "./summit_log";
import type { Waypoint } from "./waypoint";
@@ -24,6 +25,7 @@ class Trail {
category?: Category;
waypoints: Waypoint[]
summit_logs: SummitLog[]
comments_via_trail: Comment[]
gpx_data?: string
}
tags?: string[];
@@ -47,6 +49,7 @@ class Trail {
category?: Category,
waypoints?: Waypoint[],
summit_logs?: SummitLog[],
comments?: Comment[],
tags?: string[],
description?: string
created?: string
@@ -71,7 +74,8 @@ class Trail {
this.expand = {
category: params?.category,
waypoints: params?.waypoints ?? [],
summit_logs: params?.summit_logs ?? []
summit_logs: params?.summit_logs ?? [],
comments_via_trail: params?.comments ?? []
}
this.tags = params?.tags ?? []
this.description = params?.description ?? "";

View File

@@ -0,0 +1,53 @@
import { Comment } from "$lib/models/comment";
import { pb } from "$lib/pocketbase";
import { ClientResponseError } from "pocketbase";
import { writable, type Writable } from "svelte/store";
export const comments: Writable<Comment[]> = writable([])
export async function comments_create(comment: Comment) {
if (!pb.authStore.model) {
throw new Error("Unauthenticated");
}
comment.author = pb.authStore.model!.id;
let r = await fetch('/api/v1/comment', {
method: 'PUT',
body: JSON.stringify(comment),
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const model: Comment = await r.json();
return model;
}
export async function comments_update(comment: Comment) {
let r = await fetch('/api/v1/comment/' + comment.id, {
method: 'POST',
body: JSON.stringify(comment),
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const model: Comment = await r.json();
return model;
}
export async function comments_delete(comment: Comment) {
const r = await fetch('/api/v1/comment/' + comment.id, {
method: 'DELETE',
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
}

View File

@@ -170,7 +170,7 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
export async function trails_show(id: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({
expand: "category,waypoints,summit_logs",
expand: "category,waypoints,summit_logs,comments_via_trail.author",
}), {
method: 'GET',
})
@@ -251,7 +251,7 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
}
}
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos: File[], gpx: File | Blob | null) {
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: File[], gpx?: File | Blob | null) {
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand.waypoints ?? [], newTrail.expand.waypoints ?? []);
@@ -306,10 +306,13 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos: Fi
formData.append("gpx", gpx);
}
for (const photo of photos) {
formData.append("photos", photo)
if (photos) {
for (const photo of photos) {
formData.append("photos", photo)
}
}
const deletedPhotos = oldTrail.photos.filter(oldPhoto => !newTrail.photos.find(newPhoto => newPhoto === oldPhoto));
for (const deletedPhoto of deletedPhotos) {

View File

@@ -0,0 +1,26 @@
import type { Comment } from '$lib/models/comment';
import { pb } from '$lib/pocketbase';
import { error, json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
try {
const r: Comment[] = await pb.collection('comments').getFullList<Comment>({
expand: "author",
sort: "-created",
})
return json(r)
} catch (e: any) {
throw error(e.status, e);
}
}
export async function PUT(event: RequestEvent) {
const data = await event.request.json();
try {
const r = await pb.collection('comments').create<Comment>(data)
return json(r);
} catch (e: any) {
throw error(e.status, e)
}
}

View File

@@ -0,0 +1,33 @@
import type { Comment } from "$lib/models/comment";
import { pb } from "$lib/pocketbase";
import { error, json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await pb.collection('comments')
.getOne<Comment>(event.params.id as string, { expand: "author" })
return json(r)
} catch (e: any) {
throw error(e.status, e);
}
}
export async function POST(event: RequestEvent) {
const data = await event.request.json()
try {
const r = await pb.collection('comments').update<Comment>(event.params.id as string, data)
return json(r);
} catch (e: any) {
throw error(e.status, e)
}
}
export async function DELETE(event: RequestEvent) {
try {
const r = await pb.collection('comments').delete(event.params.id as string)
return json({ 'acknowledged': r });
} catch (e: any) {
throw error(e.status, e)
}
}

View File

@@ -1,6 +1,6 @@
import type { List } from "$lib/models/list";
import { pb } from "$lib/pocketbase";
import { error, json, type RequestEvent } from "@sveltejs/kit";
import type { List } from "postcss/lib/list";
export async function GET(event: RequestEvent) {
try {