Merge branch 'feature'
This commit is contained in:
64
web/src/lib/components/comment/comment_card.svelte
Normal file
64
web/src/lib/components/comment/comment_card.svelte
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"change": "更换",
|
||||
"changelog": "变更日志",
|
||||
"chinese": "华人",
|
||||
"comments": "",
|
||||
"completed": "已完成",
|
||||
"completion-status": "完成状态",
|
||||
"contribute": "贡献",
|
||||
|
||||
19
web/src/lib/models/comment.ts
Normal file
19
web/src/lib/models/comment.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 ?? "";
|
||||
|
||||
53
web/src/lib/stores/comment_store.ts
Normal file
53
web/src/lib/stores/comment_store.ts
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
26
web/src/routes/api/v1/comment/+server.ts
Normal file
26
web/src/routes/api/v1/comment/+server.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
33
web/src/routes/api/v1/comment/[id]/+server.ts
Normal file
33
web/src/routes/api/v1/comment/[id]/+server.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user