Improve mobile version
This commit is contained in:
parent
fdab17c8e2
commit
2e6f453abe
14 changed files with 421 additions and 73 deletions
|
|
@ -15,9 +15,10 @@
|
|||
type Props = {
|
||||
expanded?: boolean;
|
||||
onToggle: () => void;
|
||||
mobileActive?: boolean;
|
||||
};
|
||||
|
||||
let { expanded = false, onToggle }: Props = $props();
|
||||
let { expanded = false, onToggle, mobileActive = false }: Props = $props();
|
||||
|
||||
let asideEl: HTMLElement;
|
||||
let listEl = $state<HTMLDivElement | null>(null);
|
||||
|
|
@ -144,14 +145,15 @@
|
|||
|
||||
<aside
|
||||
bind:this={asideEl}
|
||||
class="absolute top-2 right-0 z-10 flex h-[calc(100%-0.5rem)] flex-col rounded-tl-xl bg-white transition-all duration-200 min-[1540px]:rounded-tr-xl
|
||||
{expanded ? 'w-150 shadow-2xl' : 'w-80 shadow-lg'} px-6 py-6"
|
||||
class="flex-1 flex-col bg-white px-4 pt-4 pb-20 md:absolute md:top-2 md:right-0 md:z-10 md:h-[calc(100%-0.5rem)] md:flex-none md:rounded-tl-xl md:px-6 md:py-6 md:transition-all md:duration-200 min-[1540px]:rounded-tr-xl
|
||||
{mobileActive ? 'flex' : 'hidden'} md:flex
|
||||
{expanded ? 'md:w-150 md:shadow-2xl' : 'md:w-80 md:shadow-lg'}"
|
||||
>
|
||||
<div class="mb-6 flex shrink-0 items-center justify-between">
|
||||
<span class="text-brand text-[1.5rem] leading-7">Chat</span>
|
||||
<button
|
||||
onclick={onToggle}
|
||||
class="rounded bg-neutral-100 transition-colors hover:bg-neutral-200"
|
||||
class="hidden rounded bg-neutral-100 transition-colors hover:bg-neutral-200 md:block"
|
||||
aria-label={expanded ? "Collapse chat" : "Expand chat"}
|
||||
>
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</script>
|
||||
|
||||
<aside
|
||||
class="flex max-w-52 min-w-48 shrink-0 flex-col justify-between py-2 pl-8 pr-1 pb-8"
|
||||
class="hidden max-w-52 min-w-48 shrink-0 flex-col justify-between py-2 pl-8 pr-1 pb-8 md:flex"
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
|
|
@ -59,6 +59,10 @@
|
|||
>
|
||||
More
|
||||
</p>
|
||||
<a
|
||||
href="/about"
|
||||
class="block py-1 text-neutral-500 hover:text-neutral-900">About</a
|
||||
>
|
||||
<a
|
||||
href="/contacts"
|
||||
class="block py-1 text-neutral-500 hover:text-neutral-900">Contacts</a
|
||||
|
|
|
|||
200
src/lib/components/MobileMenu.svelte
Normal file
200
src/lib/components/MobileMenu.svelte
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<script lang="ts">
|
||||
import { fly, fade } from "svelte/transition";
|
||||
import { rooms } from "$lib/mock";
|
||||
import { auth, openLogin, logout } from "$lib/auth.svelte";
|
||||
import { draftState, resumeDraft } from "$lib/draft.svelte";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
mode: "simple" | "full";
|
||||
activeRoom?: string;
|
||||
};
|
||||
|
||||
let { open, onClose, mode, activeRoom }: Props = $props();
|
||||
|
||||
const groups = [...new Set(rooms.map((r) => r.group))];
|
||||
|
||||
function onLogin() {
|
||||
onClose();
|
||||
openLogin();
|
||||
}
|
||||
|
||||
function onLogout() {
|
||||
if (confirm("Log out?")) {
|
||||
logout();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function onResume() {
|
||||
onClose();
|
||||
resumeDraft();
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (!open) return;
|
||||
if (e.key === "Escape") onClose();
|
||||
}
|
||||
|
||||
// Swipe-to-close: the drawer sits on the right, so a rightward swipe dismisses it.
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let swiping = false;
|
||||
|
||||
function onTouchStart(e: TouchEvent) {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
swiping = false;
|
||||
}
|
||||
|
||||
function onTouchMove(e: TouchEvent) {
|
||||
const dx = e.touches[0].clientX - touchStartX;
|
||||
const dy = e.touches[0].clientY - touchStartY;
|
||||
if (Math.abs(dx) > Math.abs(dy)) swiping = true;
|
||||
}
|
||||
|
||||
function onTouchEnd(e: TouchEvent) {
|
||||
if (!swiping) return;
|
||||
if (e.changedTouches[0].clientX - touchStartX > 60) onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeydown} />
|
||||
|
||||
{#if open}
|
||||
<div class="fixed inset-0 z-40 md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close menu"
|
||||
class="absolute inset-0 bg-black/40"
|
||||
onclick={onClose}
|
||||
transition:fade={{ duration: 150 }}
|
||||
></button>
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 flex w-72 max-w-[80%] flex-col bg-white px-6 py-4 shadow-xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Menu"
|
||||
tabindex="-1"
|
||||
ontouchstart={onTouchStart}
|
||||
ontouchmove={onTouchMove}
|
||||
ontouchend={onTouchEnd}
|
||||
transition:fly={{ x: 300, duration: 200 }}
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="-mr-1 rounded p-1 text-neutral-500 hover:bg-neutral-100"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-7 w-7"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-col overflow-y-auto">
|
||||
<a
|
||||
href="/"
|
||||
onclick={onClose}
|
||||
class="py-2 text-xl text-neutral-700 hover:text-brand">Home</a
|
||||
>
|
||||
|
||||
{#if mode === "full"}
|
||||
<nav class="mt-4">
|
||||
{#each groups as group}
|
||||
<div class="mb-5">
|
||||
<p
|
||||
class="pb-1 text-xs font-semibold uppercase tracking-wider text-neutral-400"
|
||||
>
|
||||
{group}
|
||||
</p>
|
||||
{#each rooms.filter((r) => r.group === group) as room}
|
||||
<a
|
||||
href="/room/{room.slug}"
|
||||
onclick={onClose}
|
||||
class="block py-1.5 text-lg
|
||||
{activeRoom === room.slug ? 'text-brand' : 'text-neutral-700 hover:text-brand'}"
|
||||
>
|
||||
{room.name}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href="/about"
|
||||
onclick={onClose}
|
||||
class="py-2 text-xl text-neutral-700 hover:text-brand">About</a
|
||||
>
|
||||
<a
|
||||
href="/contacts"
|
||||
onclick={onClose}
|
||||
class="py-2 text-xl text-neutral-700 hover:text-brand">Contacts</a
|
||||
>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 border-t border-neutral-100 pt-4">
|
||||
{#if draftState.iconized}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onResume}
|
||||
class="w-full rounded bg-brand px-3 py-2 font-medium text-white hover:bg-brand-hover"
|
||||
>
|
||||
Resume draft
|
||||
</button>
|
||||
{/if}
|
||||
{#if auth.user}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onLogout}
|
||||
class="flex w-full items-center gap-2 rounded px-2 py-2 text-left text-neutral-700 hover:bg-neutral-100"
|
||||
aria-label="Log out"
|
||||
>
|
||||
{#if auth.user.metadata.picture}
|
||||
<img
|
||||
src={auth.user.metadata.picture}
|
||||
alt=""
|
||||
class="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<span
|
||||
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-sm font-semibold text-neutral-600"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{auth.user.shortName.slice(0, 1).toUpperCase()}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="truncate text-lg font-medium">{auth.user.shortName}</span
|
||||
>
|
||||
<span class="ml-auto shrink-0 text-sm text-neutral-400">Log out</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onLogin}
|
||||
class="w-full rounded bg-brand px-3 py-2 text-lg font-medium text-white hover:bg-brand-hover"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -2,24 +2,53 @@
|
|||
import { groupStore } from "$lib/group.svelte";
|
||||
import { GROUP_ID } from "$lib/config";
|
||||
|
||||
type Props = { onMenuToggle: () => void };
|
||||
let { onMenuToggle }: Props = $props();
|
||||
|
||||
const name = $derived(groupStore.data?.name ?? GROUP_ID);
|
||||
</script>
|
||||
|
||||
<header class="flex h-16 shrink-0 items-center justify-between px-8">
|
||||
<div class="flex items-center gap-2">
|
||||
<header
|
||||
class="sticky top-0 z-20 flex h-16 shrink-0 items-center justify-between gap-2 bg-neutral-100 px-4 md:static md:px-8"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
{#if groupStore.data?.picture}
|
||||
<img
|
||||
src={groupStore.data.picture}
|
||||
alt=""
|
||||
class="h-9 w-9 rounded-full object-cover"
|
||||
class="h-9 w-9 shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-9 w-9 items-center justify-center rounded-full bg-red-500 font-bold text-white"
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-red-500 font-bold text-white"
|
||||
>
|
||||
{name[0].toUpperCase()}
|
||||
</div>
|
||||
{/if}
|
||||
<span class="text-[1.7rem] font-medium text-neutral-900">{name}</span>
|
||||
<span class="truncate text-2xl font-medium text-neutral-900 md:text-[1.7rem]"
|
||||
>{name}</span
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onMenuToggle}
|
||||
class="-mr-1 shrink-0 rounded p-1.5 text-neutral-700 hover:bg-neutral-200 md:hidden"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@
|
|||
<svelte:window onkeydown={onKeydown} />
|
||||
|
||||
{#if draftState.modalOpen}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-0 md:p-4">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Minimize draft"
|
||||
|
|
@ -130,7 +130,7 @@
|
|||
onclick={iconizeDraft}
|
||||
></button>
|
||||
<div
|
||||
class="relative w-full max-w-2xl rounded-lg bg-white p-6 shadow-xl flex flex-col gap-4 max-h-[90vh] overflow-hidden"
|
||||
class="relative flex h-full max-h-full w-full flex-col gap-4 overflow-y-auto bg-white p-6 shadow-xl md:h-auto md:max-h-[90vh] md:max-w-2xl md:overflow-hidden md:rounded-lg"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="newdisc-title"
|
||||
|
|
@ -284,7 +284,7 @@
|
|||
type="button"
|
||||
onclick={onDiscard}
|
||||
disabled={draftState.publishing}
|
||||
class="rounded bg-neutral-700 px-5 py-1.5 text-sm font-medium text-white hover:bg-neutral-800 disabled:opacity-50"
|
||||
class="rounded bg-neutral-700 px-5 py-1.5 font-medium text-white hover:bg-neutral-800 disabled:opacity-50"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
|
|
@ -294,7 +294,7 @@
|
|||
disabled={draftState.publishing ||
|
||||
!draftState.title.trim() ||
|
||||
!draftState.content.trim()}
|
||||
class="rounded bg-brand px-5 py-1.5 text-sm font-medium text-white hover:bg-brand-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded bg-brand px-5 py-1.5 font-medium text-white hover:bg-brand-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{draftState.publishing ? "Publishing…" : "Publish discussion"}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
aria-label="Sort discussions, current: {currentLabel}"
|
||||
class="flex items-center gap-2 rounded bg-neutral-800 px-4 py-1.5 text-sm font-medium text-white hover:bg-neutral-700"
|
||||
class="flex items-center gap-2 rounded bg-neutral-800 px-4 py-1.5 md:text-sm font-medium text-white hover:bg-neutral-700"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<a
|
||||
href="/thread/{thread.id}"
|
||||
class="flex items-center gap-6 border-b border-neutral-100 py-4 hover:bg-neutral-50"
|
||||
class="flex items-center gap-3 border-b border-neutral-100 py-4 hover:bg-neutral-50 sm:gap-6"
|
||||
>
|
||||
<!-- Col 1: title + byline -->
|
||||
<div class="flex-1 min-w-0">
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Col 3: replies | activity -->
|
||||
<div class="flex shrink-0 items-center gap-6">
|
||||
<div class="flex shrink-0 items-center gap-4 sm:gap-6">
|
||||
<div class="flex flex-col items-center gap-0.5">
|
||||
<span class="text-neutral-800">{thread.replyCount}</span>
|
||||
<span class="text-sm text-neutral-400">replies</span>
|
||||
|
|
|
|||
|
|
@ -141,10 +141,10 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="sticky self-start flex-shrink-0 flex flex-col items-end select-none pt-1"
|
||||
style="top: calc({topOffset}px - 1.5rem); height: 50vh; width: 72px; display: {isOverflowing
|
||||
class="sticky self-start flex-shrink-0 flex-col items-end select-none pt-1 max-md:hidden {isOverflowing
|
||||
? 'flex'
|
||||
: 'none'};"
|
||||
: 'hidden'}"
|
||||
style="top: calc({topOffset}px - 1.5rem); height: 50vh; width: 72px;"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<!-- First post date -->
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import favicon from "$lib/assets/favicon.svg";
|
||||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import LeftSidebar from "$lib/components/LeftSidebar.svelte";
|
||||
import MobileMenu from "$lib/components/MobileMenu.svelte";
|
||||
import ChatSidebar from "$lib/components/ChatSidebar.svelte";
|
||||
import LoginModal from "$lib/components/LoginModal.svelte";
|
||||
import JoinModal from "$lib/components/JoinModal.svelte";
|
||||
|
|
@ -27,8 +28,17 @@
|
|||
});
|
||||
|
||||
let chatExpanded = $state(false);
|
||||
let menuOpen = $state(false);
|
||||
let mobileView = $state<"forum" | "chat">("forum");
|
||||
|
||||
const activeRoom = $derived(page.params.slug ?? "");
|
||||
|
||||
// On mobile a route change should always land on the forum pane, so opening
|
||||
// a thread or room from the menu never leaves the user stranded on chat.
|
||||
$effect(() => {
|
||||
page.url.pathname;
|
||||
mobileView = "forum";
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -36,35 +46,98 @@
|
|||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex h-screen items-center justify-center px-8 text-center text-gray-700 md:hidden"
|
||||
role="status"
|
||||
class="mx-auto flex max-w-[1540px] flex-col md:h-screen md:overflow-hidden {mobileView ===
|
||||
'chat'
|
||||
? 'h-dvh overflow-hidden'
|
||||
: ''}"
|
||||
>
|
||||
Mobile version is coming, for now use a desktop PC
|
||||
</div>
|
||||
|
||||
<Navbar onMenuToggle={() => (menuOpen = true)} />
|
||||
<div
|
||||
class="hidden h-screen flex-col overflow-hidden max-w-[1540px] mx-auto md:flex"
|
||||
class="relative md:flex md:flex-1 md:overflow-hidden md:gap-5 md:pt-2 {mobileView ===
|
||||
'chat'
|
||||
? 'flex flex-1 overflow-hidden'
|
||||
: ''}"
|
||||
>
|
||||
<Navbar />
|
||||
<div class="relative 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 pt-6 pb-20 shadow-lg"
|
||||
class="min-h-[calc(100dvh_-_4rem)] bg-white px-6 pt-8 pb-20 shadow-lg md:min-h-0 md:flex-1 md:overflow-y-auto md:rounded-t-xl md:px-10 md:pt-6
|
||||
{mobileView === 'chat' ? 'hidden md:block' : 'block'}"
|
||||
>
|
||||
{@render children()}
|
||||
</main>
|
||||
{#if chatEnabled}
|
||||
<div class="shrink-0 w-80" aria-hidden="true"></div>
|
||||
<div class="hidden w-80 shrink-0 md:block" aria-hidden="true"></div>
|
||||
<ChatSidebar
|
||||
expanded={chatExpanded}
|
||||
onToggle={() => (chatExpanded = !chatExpanded)}
|
||||
mobileActive={mobileView === "chat"}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if chatEnabled}
|
||||
<nav
|
||||
class="fixed inset-x-0 bottom-0 z-30 flex border-t border-neutral-200 bg-neutral-100 md:hidden"
|
||||
aria-label="Switch view"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mobileView = "forum")}
|
||||
aria-pressed={mobileView === "forum"}
|
||||
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
|
||||
{mobileView === 'forum' ? 'text-brand' : 'text-neutral-500'}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
Forum
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mobileView = "chat")}
|
||||
aria-pressed={mobileView === "chat"}
|
||||
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
|
||||
{mobileView === 'chat' ? 'text-brand' : 'text-neutral-500'}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
Chat
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="hidden md:contents">
|
||||
<MobileMenu
|
||||
open={menuOpen}
|
||||
onClose={() => (menuOpen = false)}
|
||||
{mode}
|
||||
{activeRoom}
|
||||
/>
|
||||
|
||||
<LoginModal />
|
||||
<JoinModal />
|
||||
<NewDiscussionModal />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -90,14 +90,14 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 py-2">
|
||||
<h1 class="text-[1.65rem] text-brand">Discussions</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={onNewTopic}
|
||||
class="rounded bg-brand px-6 py-1.5 text-sm font-medium text-white hover:bg-brand-hover"
|
||||
class="rounded bg-brand px-4 py-1.5 md:text-sm font-medium text-white hover:bg-brand-hover md:px-6"
|
||||
>
|
||||
New Topic
|
||||
New discussion
|
||||
</button>
|
||||
<SortToggle {sort} />
|
||||
</div>
|
||||
|
|
|
|||
21
src/routes/about/+page.svelte
Normal file
21
src/routes/about/+page.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { groupStore } from "$lib/group.svelte";
|
||||
import { GROUP_ID } from "$lib/config";
|
||||
|
||||
const name = $derived(groupStore.data?.name ?? GROUP_ID);
|
||||
const about = $derived(groupStore.data?.about ?? "");
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>About — {name}</title>
|
||||
<meta name="description" content={about} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<h1 class="py-2 text-[1.65rem] text-brand">About {name}</h1>
|
||||
{#if about}
|
||||
<p class="leading-relaxed whitespace-pre-wrap text-neutral-600">{about}</p>
|
||||
{:else}
|
||||
<p class="text-neutral-400">No description available.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
<meta name="description" content="Admins and contacts for this community." />
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<h1 class="py-2 text-[1.65rem] text-brand">Contacts</h1>
|
||||
|
||||
{#if !groupStore.data}
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@
|
|||
<h1 class="text-[1.65rem] text-brand leading-7">{room?.name ?? slug}</h1>
|
||||
<button
|
||||
onclick={onNewTopic}
|
||||
class="rounded bg-brand px-6 py-1.5 text-sm font-medium text-white hover:bg-brand-hover"
|
||||
class="rounded bg-brand px-6 py-1.5 md:text-sm font-medium text-white hover:bg-brand-hover"
|
||||
>
|
||||
New Topic
|
||||
New discussion
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,13 @@
|
|||
};
|
||||
main.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
// On mobile the document scrolls (not main), so clear the quote popup on
|
||||
// window scroll too.
|
||||
const onWinScroll = () => {
|
||||
if (selectionTarget) selectionTarget = null;
|
||||
};
|
||||
window.addEventListener("scroll", onWinScroll, { passive: true });
|
||||
|
||||
const onSelectionChange = () => {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
|
||||
|
|
@ -171,6 +178,7 @@
|
|||
return () => {
|
||||
main.classList.remove("no-scrollbar");
|
||||
main.removeEventListener("scroll", onScroll);
|
||||
window.removeEventListener("scroll", onWinScroll);
|
||||
document.removeEventListener("selectionchange", onSelectionChange);
|
||||
};
|
||||
});
|
||||
|
|
@ -218,13 +226,22 @@
|
|||
bindEl: (el: HTMLElement | null) => void,
|
||||
)}
|
||||
{@const author = resolveAuthor(p.pubkey, profiles)}
|
||||
<div use:bindEl class="flex gap-6 items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div use:bindEl class="md:flex md:gap-6 md:items-start">
|
||||
<div class="hidden flex-shrink-0 md:block">
|
||||
{@render avatar(author)}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-baseline justify-between mb-3">
|
||||
<span class="font-medium text-neutral-400">{author.name}</span>
|
||||
<div
|
||||
class="mb-4 flex items-center justify-between md:mb-3 md:items-baseline"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div class="flex-shrink-0 md:hidden">
|
||||
{@render avatar(author)}
|
||||
</div>
|
||||
<span class="truncate font-medium text-neutral-400"
|
||||
>{author.name}</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-sm text-neutral-400 ml-4 flex-shrink-0"
|
||||
>{formatDate(p.createdAt)}</span
|
||||
>
|
||||
|
|
@ -232,6 +249,7 @@
|
|||
<div data-quote-post-index={index} id="post-{p.id}" class="scroll-mt-32">
|
||||
<PostContent content={p.content} {profiles} {threadEventAuthors} />
|
||||
</div>
|
||||
{#if auth.user}
|
||||
<div class="flex items-center justify-start mt-6">
|
||||
<button
|
||||
onclick={() => quotePost(p)}
|
||||
|
|
@ -254,15 +272,16 @@
|
|||
>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if detail}
|
||||
<div class="flex gap-6 items-start">
|
||||
<div class="flex-1 min-w-0" class:pr-18={!scrubberVisible}>
|
||||
<div class="flex-1 min-w-0 {!scrubberVisible ? 'md:pr-18' : ''}">
|
||||
<div
|
||||
class="sticky -top-6 bg-white z-10 pb-1 -mx-10 px-10 pt-6 -mt-6 relative"
|
||||
class="relative z-10 bg-white pb-1 md:sticky md:-top-6 md:-mx-10 md:px-10 md:pt-6 md:-mt-6"
|
||||
>
|
||||
<h1 class="text-[1.65rem] text-brand leading-7">{detail.title}</h1>
|
||||
{#if isScrolled}
|
||||
|
|
@ -295,7 +314,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-8 border-t border-neutral-200 pt-6 pl-18">
|
||||
<div class="mt-8 border-t border-neutral-200 pt-6 md:pl-18">
|
||||
{#if auth.user}
|
||||
{#if replyError}
|
||||
<div
|
||||
|
|
@ -314,13 +333,13 @@
|
|||
contextPubkeys={allPosts.map((p) => p.pubkey)}
|
||||
{threadEventAuthors}
|
||||
/>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
onclick={submitReply}
|
||||
disabled={replying || !replyContent.trim()}
|
||||
class="rounded bg-brand px-6 py-1.5 font-medium text-white hover:bg-brand-hover disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{replying ? "Posting…" : "Reply"}
|
||||
{replying ? "Posting…" : "post reply"}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue