Skip to content
Merged
28 changes: 19 additions & 9 deletions platforms/pictique/src/lib/fragments/Drawer/Drawer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@
interface IDrawerProps extends HTMLAttributes<HTMLDivElement> {
drawer?: CupertinoPane;
children?: Snippet;
onClose?: () => void;
}

let { drawer = $bindable(), children = undefined, ...restProps }: IDrawerProps = $props();
let {
drawer = $bindable(),
children = undefined,
onClose,
...restProps
}: IDrawerProps = $props();

let drawerElement: HTMLElement;

Expand Down Expand Up @@ -46,29 +52,33 @@
bottomClose: true,
buttonDestroy: false,
cssClass: '',
initialBreak: 'middle',
initialBreak: 'top',
breaks: {
top: { enabled: true, height: window.innerHeight * 0.9 },
middle: { enabled: true, height: window.innerHeight * 0.5 }
},
events: {
onBackdropTap: () => dismiss()
onBackdropTap: () => dismiss(),
onWillDismiss: () => onClose?.()
}
});
});
</script>

<div bind:this={drawerElement} {...restProps} {...swipeActions} class={cn(restProps.class)}>
<div class="h-[100%] overflow-y-scroll">
{@render children?.()}
</div>
{@render children?.()}
</div>

<style>
:global(.pane) {
border-top-left-radius: 32px !important;
border-top-right-radius: 32px !important;
padding: 20px !important;
overflow-y: scroll !important;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
::-webkit-scrollbar {
display: none !important;
}
}
:global(.pane)::-webkit-scrollbar {
display: none !important;
}
</style>
52 changes: 35 additions & 17 deletions platforms/pictique/src/lib/fragments/Header/Header.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
<script lang="ts">
import { page } from '$app/state';
import { cn } from '$lib/utils';
import { ArrowLeft01Icon, ArrowLeft02Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/svelte';
import type { HTMLAttributes } from 'svelte/elements';

interface IHeaderProps extends HTMLAttributes<HTMLElement> {
variant: 'primary' | 'secondary' | 'tertiary';
heading?: string;
isCallBackNeeded?: boolean;
callback?: () => void;
options?: { name: string; handler: () => void }[];
}
const { ...restProps }: HTMLAttributes<HTMLElement> = $props();

const { variant, isCallBackNeeded, callback, heading, ...restProps }: IHeaderProps = $props();
let route = $derived(page.url.pathname);
let heading = $state('');

const variantClasses = {
$effect(() => {
if (route.includes('home')) {
heading = 'Feed';
} else if (route.includes('/discover')) {
heading = 'Search';
} else if (route.includes('/post/audience')) {
heading = 'Audience';
} else if (route.includes('/post')) {
heading = 'Upload photo';
} else if (route === '/messages') {
heading = 'Messages';
} else if (route.includes('/settings')) {
heading = 'Settings';
} else if (route.includes('/profile')) {
heading = 'Profile';
}
});

type Variant = 'primary' | 'secondary' | 'tertiary';

let variant = $derived.by((): Variant => {
if (route === `/messages/${page.params.id}` || route.includes('/post')) {
return 'secondary';
}
if (route.includes('profile')) {
return 'tertiary';
}
return 'primary';
});

const variantClasses: Record<Variant, { text: string; background: string }> = {
primary: {
text: 'text-transparent bg-clip-text bg-[image:var(--color-brand-gradient)] py-2',
background: ''
Expand Down Expand Up @@ -76,14 +102,6 @@
</h1>
{/if}
</span>
{#if isCallBackNeeded}
<button
class={cn(['cursor-pointer rounded-full p-2 hover:bg-gray-100', classes.background])}
onclick={callback}
aria-label="Callback"
>
</button>
{/if}
</header>

<!--
Expand Down
30 changes: 30 additions & 0 deletions platforms/pictique/src/lib/fragments/MainPanel/MainPanel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import Header from '../Header/Header.svelte';
import type { Snippet } from 'svelte';

interface IMainPanelProps extends HTMLAttributes<HTMLDivElement> {
children: Snippet;
RightPanel?: Snippet;
}

let { children, RightPanel }: IMainPanelProps = $props();
</script>

<div class="flex flex-col md:h-dvh md:flex-row">
<section
class="hide-scrollbar min-w-0 flex-1 overflow-y-auto px-4 pb-8 md:h-dvh md:px-8 md:pt-8"
>
<div class="flex flex-col">
<Header />
{@render children()}
</div>
</section>
{#if RightPanel}
<aside
class="hide-scrollbar relative hidden h-dvh w-[30vw] overflow-y-scroll border border-y-0 border-e-0 border-s-gray-200 px-8 pt-12 md:flex md:flex-col"
>
{@render RightPanel?.()}
</aside>
{/if}
</div>
18 changes: 10 additions & 8 deletions platforms/pictique/src/lib/fragments/RightAside/RightAside.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
import type { HTMLAttributes } from 'svelte/elements';

interface IRightAsideProps extends HTMLAttributes<HTMLElement> {
header: Snippet;
asideContent: Snippet;
header?: Snippet;
}
let { header, asideContent, ...restProps }: IRightAsideProps = $props();
let { header, children, ...restProps }: IRightAsideProps = $props();
</script>

<aside {...restProps} class="hidden border border-y-0 border-s-gray-200 md:block md:pt-13">
<div class="mx-5">
<aside
{...restProps}
class="hide-scrollbar relative hidden h-dvh w-[30vw] overflow-y-scroll border border-y-0 border-e-0 border-s-gray-200 pl-8 md:flex md:flex-col"
>
{#if header}
<h2 class="mb-10 text-lg font-semibold">
{@render header?.()}
</h2>
<div>
{@render asideContent?.()}
</div>
{/if}
<div>
{@render children?.()}
</div>
</aside>
4 changes: 1 addition & 3 deletions platforms/pictique/src/lib/ui/Avatar/Avatar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,14 @@
};

const classes = $derived({
common: cn('rounded-full'),
common: cn('rounded-full shrink-0 aspect-square object-cover'),
size: sizeVariant[size] || sizeVariant.md
});

function handleError() {
hasError = true;
img = '/images/user.png';
}

$inspect(img);
</script>

{#if hasError || !img}
Expand Down
138 changes: 7 additions & 131 deletions platforms/pictique/src/routes/(protected)/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { BottomNav, Comment, Header, MessageInput, SideBar } from '$lib/fragments';
import { BottomNav, SideBar } from '$lib/fragments';
import CreatePostModal from '$lib/fragments/CreatePostModal/CreatePostModal.svelte';
import { showComments } from '$lib/store/store.svelte';
import { activePostId, comments, createComment, fetchComments } from '$lib/stores/comments';
import { closeDisclaimerModal, isDisclaimerModalOpen } from '$lib/stores/disclaimer';
import { isCreatePostModalOpen, openCreatePostModal } from '$lib/stores/posts';
import type { userProfile } from '$lib/types';
Expand All @@ -13,71 +11,16 @@
import { removeAuthId, removeAuthToken } from '$lib/utils';
import type { AxiosError } from 'axios';
import { onMount } from 'svelte';
import { heading } from '../store';

let { children } = $props();
let ownerId: string | null = $state(null);
let route = $derived(page.url.pathname);

let commentValue: string = $state('');
let commentInput: HTMLInputElement | undefined = $state();
let idFromParams = $state();
let isCommentsLoading = $state(false);
let commentsError = $state<string | null>(null);
let profile = $state<userProfile | null>(null);
let confirmedDisclaimer = $state(false);

const handleSend = async () => {
console.log($activePostId, commentValue);
if (!$activePostId || !commentValue.trim()) return;

try {
await createComment($activePostId, commentValue);
commentValue = '';
} catch (err) {
console.error('Failed to create comment:', err);
}
};

$effect(() => {
idFromParams = page.params.id;

console.log(route);

if (route.includes('home')) {
heading.set('Feed');
} else if (route.includes('discover')) {
heading.set('Search');
} else if (route.includes('/post/audience')) {
heading.set('Audience');
} else if (route.includes('post')) {
heading.set('Upload photo');
} else if (route === '/messages') {
heading.set('Messages');
} else if (route.includes('settings')) {
heading.set('Settings');
} else if (route.includes('profile')) {
heading.set('Profile');
}
});

// Watch for changes in showComments to fetch comments when opened
$effect(() => {
ownerId = getAuthId();
if (showComments.value && activePostId) {
isCommentsLoading = true;
commentsError = null;
fetchComments($activePostId as string)
.catch((err) => {
commentsError = err.message;
})
.finally(() => {
isCommentsLoading = false;
});
}
});

async function fetchProfile() {
ownerId = getAuthId();
try {
if (!getAuthToken()) {
goto('/auth');
Expand All @@ -97,83 +40,16 @@
onMount(fetchProfile);
</script>

<main
class={`block h-[100dvh] ${route !== '/home' && route !== '/messages' && route !== '/profile' && !route.includes('settings') && !route.includes('/profile') ? 'grid-cols-[20vw_auto]' : 'grid-cols-[20vw_auto_30vw]'} md:grid`}
>
<main class="block h-dvh grid-cols-[20vw_1fr] md:grid">
<SideBar
profileSrc={profile?.avatarUrl || '/images/user.png'}
handlePost={async () => {
openCreatePostModal();
}}
/>
<section class="hide-scrollbar h-[100dvh] overflow-y-auto px-4 pb-8 md:px-8 md:pt-8">
<div class="flex flex-col">
<Header
variant={route === `/messages/${idFromParams}` || route.includes('/post')
? 'secondary'
: route.includes('profile')
? 'tertiary'
: 'primary'}
heading={$heading}
isCallBackNeeded={route.includes('profile')}
callback={() => alert('Ads')}
options={[
{ name: 'Report', handler: () => alert('report') },
{ name: 'Clear chat', handler: () => alert('clear') }
]}
/>
{@render children()}
</div>
</section>
{#if route === '/home' || route === '/messages'}
<aside
class="hide-scrollbar relative hidden h-[100dvh] overflow-y-scroll border border-e-0 border-t-0 border-b-0 border-s-gray-200 px-8 pt-14 md:block"
>
{#if route === '/home'}
{#if showComments.value}
<ul class="pb-4">
<h3 class="text-black-600 mb-6 text-center">{$comments.length} Comments</h3>
{#if isCommentsLoading}
<li class="text-center text-gray-500">Loading comments...</li>
{:else if commentsError}
<li class="text-center text-red-500">{commentsError}</li>
{:else}
{#each $comments as comment (comment.id)}
<li class="mb-4">
<Comment
comment={{
userImgSrc: comment.author.avatarUrl,
name: comment.author.name || comment.author.handle,
commentId: comment.id,
comment: comment.text,
isUpVoted: false,
isDownVoted: false,
upVotes: 0,
time: new Date(comment.createdAt).toLocaleDateString(),
replies: []
}}
handleReply={() => {
commentInput?.focus();
}}
/>
</li>
{/each}
{/if}
<MessageInput
class="sticky start-0 bottom-4 mt-4 w-full px-2"
variant="comment"
src={profile?.avatarUrl ?? '/images/user.png'}
bind:value={commentValue}
{handleSend}
bind:input={commentInput}
/>
</ul>
{/if}
{/if}
</aside>
{/if}
{@render children()}

{#if route !== `/messages/${idFromParams}`}
{#if !route.match(/^\/messages\/[^/]+$/)}
<BottomNav class="btm-nav" profileSrc={profile?.avatarUrl ?? ''} />
{/if}
</main>
Expand All @@ -197,8 +73,8 @@
core concepts of the W3DS ecosystem.
</p>
<p>
<b>It is not a production-grade platform</b> and may lack full reliability, performance,
and security guarantees.
<b>It is not a production-grade platform</b> and may lack full reliability, performance, and
security guarantees.
</p>
<p>
We <b>strongly recommend</b> that you avoid sharing <b>sensitive or private content</b>,
Expand Down
Loading