First templates
This commit is contained in:
parent
d3701a728a
commit
68f25fd20f
12 changed files with 786 additions and 8 deletions
78
src/lib/components/ChatSidebar.svelte
Normal file
78
src/lib/components/ChatSidebar.svelte
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { chatMessages } from "$lib/mock";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
expanded?: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { expanded = false, onToggle }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
class="flex shrink-0 flex-col rounded-tl-xl bg-white transition-all duration-200
|
||||||
|
{expanded ? 'w-[420px]' : 'w-80'}"
|
||||||
|
>
|
||||||
|
<div class="flex h-12 shrink-0 items-center justify-between px-4">
|
||||||
|
<span class="font-semibold text-gray-900">Chat</span>
|
||||||
|
<button
|
||||||
|
onclick={onToggle}
|
||||||
|
class="text-gray-400 hover:text-gray-600"
|
||||||
|
aria-label="Toggle chat size"
|
||||||
|
>
|
||||||
|
{#if expanded}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto p-3 space-y-3">
|
||||||
|
{#each chatMessages as msg}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<img
|
||||||
|
src={msg.author.picture}
|
||||||
|
alt={msg.author.name}
|
||||||
|
class="h-7 w-7 shrink-0 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs font-semibold text-gray-900"
|
||||||
|
>{msg.author.name}</span
|
||||||
|
>
|
||||||
|
<p class=" text-gray-700">{msg.content}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 p-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Message..."
|
||||||
|
class="w-full rounded border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
74
src/lib/components/LeftSidebar.svelte
Normal file
74
src/lib/components/LeftSidebar.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { rooms, community } from "$lib/mock";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
mode: "simple" | "full";
|
||||||
|
activeRoom?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { mode, activeRoom }: Props = $props();
|
||||||
|
|
||||||
|
const groups = [...new Set(rooms.map((r) => r.group))];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
class="flex max-w-52 min-w-48 shrink-0 flex-col justify-between py-2 pl-8 pr-3 pb-8"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="flex items-center gap-2 py-1 hover:bg-gray-100 text-gray-700}"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{#if mode === "simple"}
|
||||||
|
<div class="flex mt-6">
|
||||||
|
<p class="text-sm text-gray-500">{community.about}</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<nav class="flex-auto mt-6">
|
||||||
|
{#each groups as group}
|
||||||
|
<div class="mb-8">
|
||||||
|
<p
|
||||||
|
class="pb-1 text-xs font-semibold uppercase tracking-wider text-gray-400"
|
||||||
|
>
|
||||||
|
{group}
|
||||||
|
</p>
|
||||||
|
{#each rooms.filter((r) => r.group === group) as room}
|
||||||
|
<a
|
||||||
|
href="/room/{room.slug}"
|
||||||
|
class="flex items-center gap-2 py-1 hover:bg-gray-100
|
||||||
|
{activeRoom === room.slug ? ' text-brand' : 'text-gray-700'}"
|
||||||
|
>
|
||||||
|
{room.name}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<nav class="flex-auto mt-6">
|
||||||
|
<div class="">
|
||||||
|
<p
|
||||||
|
class="pb-1 text-xs font-semibold uppercase tracking-wider text-gray-400"
|
||||||
|
>
|
||||||
|
More
|
||||||
|
</p>
|
||||||
|
<a href="/settings" class="block py-1 text-gray-500 hover:text-gray-900"
|
||||||
|
>Settings</a
|
||||||
|
>
|
||||||
|
<a href="/contacts" class="block py-1 text-gray-500 hover:text-gray-900"
|
||||||
|
>Contacts</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="mt-3 w-full rounded bg-brand px-3 py-1.5 font-medium text-sm text-white hover:bg-brand-hover"
|
||||||
|
>
|
||||||
|
Join with Nostr
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
18
src/lib/components/Navbar.svelte
Normal file
18
src/lib/components/Navbar.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { community } from "$lib/mock";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="flex h-16 shrink-0 items-center justify-between px-8">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="flex h-9 w-9 items-center justify-center rounded-full bg-red-500 font-bold text-white"
|
||||||
|
>
|
||||||
|
{community.name[0]}
|
||||||
|
</div>
|
||||||
|
<span class="text-[1.7rem] font-medium text-gray-900">{community.name}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button class="text-sm text-gray-500 hover:text-gray-900"
|
||||||
|
>Create your own community</button
|
||||||
|
>
|
||||||
|
</header>
|
||||||
31
src/lib/components/Reactions.svelte
Normal file
31
src/lib/components/Reactions.svelte
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Reaction } from "$lib/mock";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
reactions: Reaction[];
|
||||||
|
zaps: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { reactions, zaps }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-1.5">
|
||||||
|
{#each reactions as r}
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1 rounded-full border border-gray-200 px-2 py-0.5 text-xs hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<span>{r.emoji}</span>
|
||||||
|
<span class="text-gray-600">{r.count}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{#if zaps > 0}
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs hover:bg-amber-100"
|
||||||
|
>
|
||||||
|
<span>⚡</span>
|
||||||
|
<span class="text-amber-700"
|
||||||
|
>{zaps >= 1000 ? (zaps / 1000).toFixed(1) + "k" : zaps}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
8
src/lib/components/Tag.svelte
Normal file
8
src/lib/components/Tag.svelte
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script lang="ts">
|
||||||
|
type Props = { label: string };
|
||||||
|
let { label }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="rounded-lg bg-accent px-2.5 py-0 text-sm text-white">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
76
src/lib/components/ThreadItem.svelte
Normal file
76
src/lib/components/ThreadItem.svelte
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Thread } from "$lib/mock";
|
||||||
|
import Tag from "./Tag.svelte";
|
||||||
|
|
||||||
|
type Props = { thread: Thread };
|
||||||
|
let { thread }: Props = $props();
|
||||||
|
|
||||||
|
// Placeholder until scoring formula is defined
|
||||||
|
const score = $derived(
|
||||||
|
thread.op.reactions.reduce((s, r) => s + r.count, 0) + thread.op.zaps,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/thread/{thread.id}"
|
||||||
|
class="flex items-center gap-6 border-b border-gray-100 py-4 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<!-- Col 1: title + byline -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-lg mb-1.5 text-gray-900 leading-6">{thread.title}</div>
|
||||||
|
<div class="flex items-center gap-1.5 text-sm text-gray-500">
|
||||||
|
<span>by</span>
|
||||||
|
<img
|
||||||
|
src={thread.op.author.picture}
|
||||||
|
alt={thread.op.author.name}
|
||||||
|
class="h-5 w-5 rounded-full"
|
||||||
|
/>
|
||||||
|
{#if thread.participants.length > 1}
|
||||||
|
<span>and</span>
|
||||||
|
<div class="flex -space-x-1.5">
|
||||||
|
{#each thread.participants
|
||||||
|
.filter((p) => p.pubkey !== thread.op.author.pubkey)
|
||||||
|
.slice(0, 4) as p}
|
||||||
|
<img
|
||||||
|
src={p.picture}
|
||||||
|
alt={p.name}
|
||||||
|
class="h-5 w-5 rounded-full ring-1 ring-white"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Col 2: tags -->
|
||||||
|
<div class="flex shrink-0 flex-wrap justify-end gap-1.5">
|
||||||
|
{#each thread.tags as tag}
|
||||||
|
<Tag label={tag.label} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Col 3: replies | score | activity -->
|
||||||
|
<div class="flex shrink-0 items-center gap-6">
|
||||||
|
<div class="flex flex-col items-center gap-0.5">
|
||||||
|
<span class=" text-gray-800">{thread.replyCount}</span>
|
||||||
|
<span class="text-sm text-gray-400">replies</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-0.5">
|
||||||
|
<span class=" text-accent">{score}</span>
|
||||||
|
<span class="text-sm text-gray-400">score</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-0.5">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<img
|
||||||
|
src={thread.participants[thread.participants.length - 1].picture}
|
||||||
|
alt={thread.participants[thread.participants.length - 1].name}
|
||||||
|
class="h-5 w-5 rounded-full"
|
||||||
|
/>
|
||||||
|
<span class=" text-gray-800">{thread.lastActivity}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-gray-400">activity</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
289
src/lib/mock.ts
Normal file
289
src/lib/mock.ts
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
export type Room = {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
group: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Author = {
|
||||||
|
pubkey: string;
|
||||||
|
name: string;
|
||||||
|
picture: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Reaction = {
|
||||||
|
emoji: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Post = {
|
||||||
|
id: string;
|
||||||
|
author: Author;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
reactions: Reaction[];
|
||||||
|
zaps: number;
|
||||||
|
replies?: Post[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Thread = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
tags: { label: string; color: string }[];
|
||||||
|
op: Post;
|
||||||
|
replyCount: number;
|
||||||
|
viewCount: number;
|
||||||
|
lastActivity: string;
|
||||||
|
participants: Author[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const community = {
|
||||||
|
name: "My big community",
|
||||||
|
about:
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rooms: Room[] = [
|
||||||
|
{ slug: "off-topics-cafe", name: "Off topics cafe", group: "Groups" },
|
||||||
|
{ slug: "proposals", name: "Proposals", group: "Groups" },
|
||||||
|
{ slug: "development", name: "Development", group: "Groups" },
|
||||||
|
{ slug: "announcements", name: "Announcements", group: "Groups" },
|
||||||
|
{ slug: "off-topics", name: "Off topics", group: "Groups" },
|
||||||
|
{ slug: "about", name: "About", group: "Resources" },
|
||||||
|
{ slug: "help-center", name: "Help center", group: "Resources" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const alice: Author = {
|
||||||
|
pubkey: "npub1alice",
|
||||||
|
name: "Alice",
|
||||||
|
picture: "https://api.dicebear.com/9.x/thumbs/svg?seed=alice",
|
||||||
|
};
|
||||||
|
const bob: Author = {
|
||||||
|
pubkey: "npub1bob",
|
||||||
|
name: "Bob",
|
||||||
|
picture: "https://api.dicebear.com/9.x/thumbs/svg?seed=bob",
|
||||||
|
};
|
||||||
|
const carol: Author = {
|
||||||
|
pubkey: "npub1carol",
|
||||||
|
name: "Carol",
|
||||||
|
picture: "https://api.dicebear.com/9.x/thumbs/svg?seed=carol",
|
||||||
|
};
|
||||||
|
const dave: Author = {
|
||||||
|
pubkey: "npub1dave",
|
||||||
|
name: "Dave",
|
||||||
|
picture: "https://api.dicebear.com/9.x/thumbs/svg?seed=dave",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const threads: Thread[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
title: "Discussion about an interesting topic",
|
||||||
|
tags: [
|
||||||
|
{ label: "general", color: "green" },
|
||||||
|
{ label: "nostr", color: "blue" },
|
||||||
|
],
|
||||||
|
op: {
|
||||||
|
id: "op1",
|
||||||
|
author: alice,
|
||||||
|
content:
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n\nProin vitae ex iaculis, luctus elit in, fermentum turpis. Pellentesque sagittis congue quam erat, egestas a lobortis non, molestie ut enim. Sed tempus augue sapien laoreet, sed iaculis velit dictum diam.\n\nMaecenas sollicitudin erat eu metus lacinia congue. Nunc sagittis laoreet odio.",
|
||||||
|
createdAt: "2025-01-15T10:00:00Z",
|
||||||
|
reactions: [
|
||||||
|
{ emoji: "👍", count: 14 },
|
||||||
|
{ emoji: "🎉", count: 3 },
|
||||||
|
],
|
||||||
|
zaps: 1200,
|
||||||
|
replies: [
|
||||||
|
{
|
||||||
|
id: "r1",
|
||||||
|
author: bob,
|
||||||
|
content:
|
||||||
|
"Etiam efficitur ornare odio, id elementum felis interdum sit amet. Nam tincidunt justo quam, eu rhoncus lectus pellentesque id.",
|
||||||
|
createdAt: "2025-01-15T11:00:00Z",
|
||||||
|
reactions: [{ emoji: "👍", count: 5 }],
|
||||||
|
zaps: 210,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "r2",
|
||||||
|
author: carol,
|
||||||
|
content:
|
||||||
|
"Maecenas sollicitudin erat eu metus lacinia congue. Nunc sagittis laoreet odio, non molestie eros.",
|
||||||
|
createdAt: "2025-01-15T12:30:00Z",
|
||||||
|
reactions: [
|
||||||
|
{ emoji: "❤️", count: 2 },
|
||||||
|
{ emoji: "😂", count: 1 },
|
||||||
|
],
|
||||||
|
zaps: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
replyCount: 12,
|
||||||
|
viewCount: 4200,
|
||||||
|
lastActivity: "4h",
|
||||||
|
participants: [alice, bob, carol, dave],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
title:
|
||||||
|
"Etiam pellentesque pulvinar quam sed interdum maximus pellentesque. Nam erat, egestas a lobortis non, molestie ut enim",
|
||||||
|
tags: [{ label: "question", color: "yellow" }],
|
||||||
|
op: {
|
||||||
|
id: "op2",
|
||||||
|
author: bob,
|
||||||
|
content:
|
||||||
|
"Etiam pellentesque pulvinar quam sed interdum. Vivamus fermentum accumsan lorem.",
|
||||||
|
createdAt: "2025-01-14T09:00:00Z",
|
||||||
|
reactions: [{ emoji: "👍", count: 2 }],
|
||||||
|
zaps: 0,
|
||||||
|
},
|
||||||
|
replyCount: 0,
|
||||||
|
viewCount: 310,
|
||||||
|
lastActivity: "1d",
|
||||||
|
participants: [bob],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
title: "Pellentesque sagittis cinque quam et blandum",
|
||||||
|
tags: [
|
||||||
|
{ label: "meta", color: "pink" },
|
||||||
|
{ label: "proposal", color: "blue" },
|
||||||
|
],
|
||||||
|
op: {
|
||||||
|
id: "op3",
|
||||||
|
author: carol,
|
||||||
|
content:
|
||||||
|
"Phasellus nec velit quis luctus maximus pellentesque. Nam erat, egestas a lobortis non, molestie ut enim.",
|
||||||
|
createdAt: "2025-01-13T14:00:00Z",
|
||||||
|
reactions: [
|
||||||
|
{ emoji: "🎉", count: 7 },
|
||||||
|
{ emoji: "👍", count: 4 },
|
||||||
|
],
|
||||||
|
zaps: 850,
|
||||||
|
},
|
||||||
|
replyCount: 7,
|
||||||
|
viewCount: 890,
|
||||||
|
lastActivity: "2d",
|
||||||
|
participants: [carol, alice, dave],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
title:
|
||||||
|
"Nam rutrum dolor in nulla maximus pellentesque. Nam erat, egestas a lobortis non, molestie ut enim imperdiet consequat",
|
||||||
|
tags: [{ label: "general", color: "green" }],
|
||||||
|
op: {
|
||||||
|
id: "op4",
|
||||||
|
author: dave,
|
||||||
|
content:
|
||||||
|
"Nam rutrum dolor in nulla imperdiet consequat. Sed tincidunt augue a velit.",
|
||||||
|
createdAt: "2025-01-12T08:00:00Z",
|
||||||
|
reactions: [],
|
||||||
|
zaps: 0,
|
||||||
|
},
|
||||||
|
replyCount: 3,
|
||||||
|
viewCount: 140,
|
||||||
|
lastActivity: "3d",
|
||||||
|
participants: [dave, bob],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
title:
|
||||||
|
"Etiam maximus pellentesque. Nam erat, egestas a lobortis non, molestie ut enim efficitur ornare odio, id elementum felis",
|
||||||
|
tags: [
|
||||||
|
{ label: "nostr", color: "blue" },
|
||||||
|
{ label: "dev", color: "green" },
|
||||||
|
],
|
||||||
|
op: {
|
||||||
|
id: "op5",
|
||||||
|
author: alice,
|
||||||
|
content:
|
||||||
|
"Etiam efficitur ornare odio, id elementum felis interdum sit amet. Nam tincidunt justo quam.",
|
||||||
|
createdAt: "2025-01-11T16:00:00Z",
|
||||||
|
reactions: [{ emoji: "👍", count: 11 }],
|
||||||
|
zaps: 340,
|
||||||
|
},
|
||||||
|
replyCount: 31,
|
||||||
|
viewCount: 4100,
|
||||||
|
lastActivity: "1h",
|
||||||
|
participants: [alice, carol, bob, dave],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
title: "Maecenas sollicitudin erat eu metus lacinia congue",
|
||||||
|
tags: [{ label: "announcement", color: "pink" }],
|
||||||
|
op: {
|
||||||
|
id: "op6",
|
||||||
|
author: carol,
|
||||||
|
content:
|
||||||
|
"Maecenas sollicitudin erat eu metus lacinia congue. Nunc sagittis laoreet odio.",
|
||||||
|
createdAt: "2025-01-10T12:00:00Z",
|
||||||
|
reactions: [{ emoji: "🎉", count: 9 }],
|
||||||
|
zaps: 500,
|
||||||
|
},
|
||||||
|
replyCount: 5,
|
||||||
|
viewCount: 620,
|
||||||
|
lastActivity: "4d",
|
||||||
|
participants: [carol, dave],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
title: "Praesent blandit bibendum lectus, nec tristique nisl",
|
||||||
|
tags: [{ label: "general", color: "green" }],
|
||||||
|
op: {
|
||||||
|
id: "op7",
|
||||||
|
author: bob,
|
||||||
|
content:
|
||||||
|
"Praesent blandit bibendum lectus, nec tristique nisl nam rutrum.",
|
||||||
|
createdAt: "2025-01-09T10:00:00Z",
|
||||||
|
reactions: [],
|
||||||
|
zaps: 0,
|
||||||
|
},
|
||||||
|
replyCount: 0,
|
||||||
|
viewCount: 95,
|
||||||
|
lastActivity: "5d",
|
||||||
|
participants: [bob],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type ChatMessage = {
|
||||||
|
id: string;
|
||||||
|
author: Author;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chatMessages: ChatMessage[] = [
|
||||||
|
{
|
||||||
|
id: "c1",
|
||||||
|
author: alice,
|
||||||
|
content:
|
||||||
|
"Quisque sagittis quam dui, nec pulvinar velit aliquam in. Sed volutpat in.",
|
||||||
|
createdAt: "2025-01-15T13:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c2",
|
||||||
|
author: bob,
|
||||||
|
content:
|
||||||
|
"Vivamus fermentum accumsan lorem, laoreet molestie lectus facilisis in.",
|
||||||
|
createdAt: "2025-01-15T13:02:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
author: carol,
|
||||||
|
content:
|
||||||
|
"Proin vitae ex iaculis, luctus elit in, fermentum turpis. Pellentesque sagittis congue quam.",
|
||||||
|
createdAt: "2025-01-15T13:05:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c4",
|
||||||
|
author: dave,
|
||||||
|
content:
|
||||||
|
"Mauro dui dolor, sagittis id sem nec, rhoncus consequat duis. Suspendisse dapibus mauris maximus mauris imperdiet.",
|
||||||
|
createdAt: "2025-01-15T13:08:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c5",
|
||||||
|
author: alice,
|
||||||
|
content:
|
||||||
|
"In hac habitasse platea dictumst. Nulla a quam tempor, posuere neque non, fringilla velit.",
|
||||||
|
createdAt: "2025-01-15T13:10:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,9 +1,38 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './layout.css';
|
import "./layout.css";
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from "$lib/assets/favicon.svg";
|
||||||
|
import Navbar from "$lib/components/Navbar.svelte";
|
||||||
|
import LeftSidebar from "$lib/components/LeftSidebar.svelte";
|
||||||
|
import ChatSidebar from "$lib/components/ChatSidebar.svelte";
|
||||||
|
import { page } from "$app/state";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
// In full mode, extract active room from the URL
|
||||||
|
const mode: "simple" | "full" = "full";
|
||||||
|
const chatEnabled = true;
|
||||||
|
|
||||||
|
let chatExpanded = $state(false);
|
||||||
|
|
||||||
|
const activeRoom = $derived(page.params.slug ?? "");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<svelte:head>
|
||||||
{@render children()}
|
<link rel="icon" href={favicon} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="flex h-screen flex-col overflow-hidden bg-gray-100">
|
||||||
|
<Navbar />
|
||||||
|
<div class="flex flex-1 gap-5 overflow-hidden pt-2">
|
||||||
|
<LeftSidebar {mode} {activeRoom} />
|
||||||
|
<main class="flex-1 overflow-y-auto rounded-t-xl bg-white px-10 py-6">
|
||||||
|
{@render children()}
|
||||||
|
</main>
|
||||||
|
{#if chatEnabled}
|
||||||
|
<ChatSidebar
|
||||||
|
{chatExpanded}
|
||||||
|
onToggle={() => (chatExpanded = !chatExpanded)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,25 @@
|
||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
import { threads } from "$lib/mock";
|
||||||
|
import ThreadItem from "$lib/components/ThreadItem.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Discussions</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<h1 class="text-[1.65rem] text-brand">Discussions</h1>
|
||||||
|
<button
|
||||||
|
class="rounded bg-brand px-6 py-1.5 text-sm font-medium text-white hover:bg-brand-hover"
|
||||||
|
>
|
||||||
|
New Topic
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#each threads as thread}
|
||||||
|
<ThreadItem {thread} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,14 @@
|
||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
@plugin '@tailwindcss/forms';
|
@plugin '@tailwindcss/forms';
|
||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-brand: #e32a6d;
|
||||||
|
--color-brand-hover: #c4205a;
|
||||||
|
--color-accent: #ffaf25;
|
||||||
|
--color-accent-hover: #e69e1a;
|
||||||
|
}
|
||||||
|
|
|
||||||
29
src/routes/room/[slug]/+page.svelte
Normal file
29
src/routes/room/[slug]/+page.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { threads, rooms } from "$lib/mock";
|
||||||
|
import ThreadItem from "$lib/components/ThreadItem.svelte";
|
||||||
|
import { page } from "$app/state";
|
||||||
|
|
||||||
|
const slug = $derived(page.params.slug);
|
||||||
|
const room = $derived(rooms.find((r) => r.slug === slug));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{room?.name ?? "Room"}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<div class="flex items-center justify-between pb-2">
|
||||||
|
<h1 class="text-[1.65rem] text-brand leading-7">{room?.name ?? slug}</h1>
|
||||||
|
<button
|
||||||
|
class="rounded bg-brand px-6 py-1.5 text-sm font-medium text-white hover:bg-brand-hover"
|
||||||
|
>
|
||||||
|
New Topic
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#each threads as thread}
|
||||||
|
<ThreadItem {thread} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
112
src/routes/thread/[id]/+page.svelte
Normal file
112
src/routes/thread/[id]/+page.svelte
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { threads } from "$lib/mock";
|
||||||
|
import Tag from "$lib/components/Tag.svelte";
|
||||||
|
import Reactions from "$lib/components/Reactions.svelte";
|
||||||
|
import { page } from "$app/state";
|
||||||
|
|
||||||
|
const thread = $derived(threads.find((t) => t.id === page.params.id));
|
||||||
|
|
||||||
|
function formatDate(iso: string) {
|
||||||
|
return new Date(iso).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{thread?.title ?? "Thread"}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if thread}
|
||||||
|
<div class="max-w-4xl">
|
||||||
|
<!-- Thread header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||||
|
<h1 class="text-[1.65rem] text-brand leading-7">{thread.title}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#each thread.tags as tag}
|
||||||
|
<Tag label={tag.label} color={tag.color} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OP -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<img
|
||||||
|
src={thread.op.author.picture}
|
||||||
|
alt={thread.op.author.name}
|
||||||
|
class="h-8 w-8 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-gray-900"
|
||||||
|
>{thread.op.author.name}</span
|
||||||
|
>
|
||||||
|
<span class="ml-2 text-xs text-gray-400"
|
||||||
|
>{formatDate(thread.op.createdAt)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prose leading-6 max-w-none text-gray-700 mb-4">
|
||||||
|
{#each thread.op.content.split("\n\n") as para}
|
||||||
|
<p>{para}</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Reactions reactions={thread.op.reactions} zaps={thread.op.zaps} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Replies -->
|
||||||
|
{#if thread.op.replies && thread.op.replies.length > 0}
|
||||||
|
<div class="border-t border-gray-200 pt-6 space-y-6">
|
||||||
|
<h2
|
||||||
|
class="text-sm font-semibold uppercase tracking-wider text-gray-400"
|
||||||
|
>
|
||||||
|
{thread.op.replies.length}
|
||||||
|
{thread.op.replies.length === 1 ? "reply" : "replies"}
|
||||||
|
</h2>
|
||||||
|
{#each thread.op.replies as reply}
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<img
|
||||||
|
src={reply.author.picture}
|
||||||
|
alt={reply.author.name}
|
||||||
|
class="h-7 w-7 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-gray-900"
|
||||||
|
>{reply.author.name}</span
|
||||||
|
>
|
||||||
|
<span class="ml-2 text-xs text-gray-400"
|
||||||
|
>{formatDate(reply.createdAt)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class=" text-gray-700 mb-2">{reply.content}</p>
|
||||||
|
<Reactions reactions={reply.reactions} zaps={reply.zaps} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Reply box -->
|
||||||
|
<div class="mt-8 border-t border-gray-200 pt-6">
|
||||||
|
<textarea
|
||||||
|
rows="4"
|
||||||
|
placeholder="Write a reply..."
|
||||||
|
class="w-full rounded border border-gray-200 px-3 py-2 focus:outline-none focus:ring-1 focus:ring-brand"
|
||||||
|
></textarea>
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
class="rounded bg-brand px-6 py-1.5 font-medium text-white hover:bg-brand-hover"
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="px-6 py-6 text-gray-500">Thread not found.</div>
|
||||||
|
{/if}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue