adds comments
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": "Ändern",
|
||||
"changelog": "Changelog",
|
||||
"chinese": "Chinesisch",
|
||||
"comments": "",
|
||||
"completed": "Abgeschlossen",
|
||||
"completion-status": "Abschlussstatus",
|
||||
"contribute": "Mitwirken",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"change": "Change",
|
||||
"changelog": "Changelog",
|
||||
"chinese": "Chinese",
|
||||
"comments": "Comments",
|
||||
"completed": "Completed",
|
||||
"completion-status": "Completion Status",
|
||||
"contribute": "Contribute",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"change": "Changement",
|
||||
"changelog": "Journal des modifications",
|
||||
"chinese": "Chinois",
|
||||
"comments": "",
|
||||
"completed": "Compléter",
|
||||
"completion-status": "L'état d'achèvement",
|
||||
"contribute": "Contribuer",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"change": "Változás",
|
||||
"changelog": "Változás napló",
|
||||
"chinese": "kínai",
|
||||
"comments": "",
|
||||
"completed": "Teljesítve",
|
||||
"completion-status": "Befejezés állapota",
|
||||
"contribute": "Hozzájárulás",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"change": "Aanpassen",
|
||||
"changelog": "Changelog",
|
||||
"chinese": "Chinese",
|
||||
"comments": "",
|
||||
"completed": "Voltooid",
|
||||
"completion-status": "Voltooiingsstatus",
|
||||
"contribute": "Bijdragen",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"change": "Zmień",
|
||||
"changelog": "Dziennik zmian",
|
||||
"chinese": "chiński",
|
||||
"comments": "",
|
||||
"completed": "Zakończono",
|
||||
"completion-status": "Stan ukończenia",
|
||||
"contribute": "Kontrybuuj",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"change": "Alterar",
|
||||
"changelog": "Registo de alterações",
|
||||
"chinese": "chinês",
|
||||
"comments": "",
|
||||
"completed": "Completada",
|
||||
"completion-status": "Status de conclusão",
|
||||
"contribute": "Contribuir",
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,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',
|
||||
})
|
||||
@@ -245,7 +245,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 ?? []);
|
||||
|
||||
@@ -300,10 +300,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) {
|
||||
|
||||
Reference in New Issue
Block a user