feat: profile view component

This commit is contained in:
ThaUnknown 2025-04-27 14:39:12 +02:00
parent 34f9a196ea
commit ef4303810f
No known key found for this signature in database
15 changed files with 193 additions and 131 deletions

View file

@ -1,6 +1,6 @@
{
"name": "ui",
"version": "6.0.14",
"version": "6.0.15",
"license": "BUSL-1.1",
"private": true,
"packageManager": "pnpm@9.14.4",

View file

@ -13,11 +13,10 @@
import { searchStore } from './SearchModal.svelte'
import { Button } from './ui/button'
import { Load } from './ui/img'
import { Profile } from './ui/profile'
import type { EpisodesResponse } from '$lib/modules/anizip/types'
import * as Avatar from '$lib/components/ui/avatar'
import * as Tooltip from '$lib/components/ui/tooltip'
import { episodes as _episodes, dedupeAiring, episodeByAirDate, notes, type Media } from '$lib/modules/anilist'
import { authAggregator, list, progress } from '$lib/modules/auth'
import { click, dragScroll } from '$lib/modules/navigate'
@ -116,18 +115,9 @@
{/if}
<div class='-space-x-1 ml-auto inline-flex pt-1 pr-0.5'>
{#each followerEntries.filter(e => e?.progress === episode) as followerEntry, i (followerEntry?.user?.id ?? i)}
<Tooltip.Root>
<Tooltip.Trigger class='inline-block size-4 cursor-default'>
<Avatar.Root class='inline-block ring-2 ring-neutral-950 size-4 bg-neutral-950'>
<Avatar.Image src={followerEntry?.user?.avatar?.medium ?? ''} alt={followerEntry?.user?.name ?? 'N/A'} />
<Avatar.Fallback>{followerEntry?.user?.name ?? 'N/A'}</Avatar.Fallback>
</Avatar.Root>
</Tooltip.Trigger>
<Tooltip.Content>
<p class='font-extrabold'>{followerEntry?.user?.name}</p>
<p class='capitalize'>{followerEntry?.status?.toLowerCase()}</p>
</Tooltip.Content>
</Tooltip.Root>
{#if followerEntry?.user}
<Profile user={followerEntry.user} class='ring-2 ring-neutral-950 size-4 bg-neutral-950' />
{/if}
{/each}
</div>
</div>

View file

@ -2,6 +2,8 @@
import dompurify from 'dompurify'
import { marked } from 'marked'
import { dragScroll } from '$lib/modules/navigate'
marked.setOptions({
gfm: true,
breaks: true,
@ -18,7 +20,7 @@
margin-block-start: .5em;
margin-block-end: .5em;
}
img {
img, video {
max-width: 100%;
-webkit-user-drag: none;
}`)
@ -55,4 +57,4 @@
export { className as class }
</script>
<div use:shadow={html} class={className} />
<div use:shadow={html} class={className} use:dragScroll />

View file

@ -14,6 +14,6 @@
<AvatarPrimitive.Image
{src}
{alt}
class={cn('aspect-square h-full w-full', className)}
class={cn('aspect-square h-full w-full object-cover', className)}
{...$$restProps}
/>

View file

@ -5,6 +5,7 @@
import { Button, iconSizes, type Props } from '$lib/components/ui/button'
import { authAggregator, fav } from '$lib/modules/auth'
import { clickwrap, keywrap } from '$lib/modules/navigate'
type $$Props = Props & { media: Media }
@ -15,6 +16,6 @@
export let variant: NonNullable<$$Props['variant']> = 'ghost'
</script>
<Button {size} {variant} class={className} on:click={() => authAggregator.toggleFav(media.id)}>
<Button {size} {variant} class={className} on:click={clickwrap(() => authAggregator.toggleFav(media.id))} on:keydown={keywrap(() => authAggregator.toggleFav(media.id))}>
<Heart fill={fav(media) ? 'currentColor' : 'transparent'} size={iconSizes[size]} />
</Button>

View file

@ -3,13 +3,13 @@
import Shadow from '../../Shadow.svelte'
import { Button, iconSizes } from '../button'
import { Profile } from '../profile'
import { Write } from '.'
import type { CommentFrag } from '$lib/modules/anilist/queries'
import type { ResultOf } from 'gql.tada'
import * as Avatar from '$lib/components/ui/avatar'
import { client } from '$lib/modules/anilist'
import { since } from '$lib/utils'
@ -28,10 +28,9 @@
<div class='rounded-md {depth % 2 === 1 ? 'bg-black' : 'bg-neutral-950'} text-secondary-foreground flex w-full py-4 px-6 flex-col'>
<div class='flex w-full justify-between text-xl'>
<div class='font-bold mb-2 line-clamp-1 flex leading-none items-center text-[16px]'>
<Avatar.Root class='inline-block size-5 mr-2'>
<Avatar.Image src={comment.user?.avatar?.medium ?? ''} alt={comment.user?.name ?? 'N/A'} />
<Avatar.Fallback>{comment.user?.name ?? 'N/A'}</Avatar.Fallback>
</Avatar.Root>
{#if comment.user}
<Profile user={comment.user} class='size-5 mr-2' />
{/if}
{comment.user?.name ?? 'N/A'}
</div>
<div class='flex ml-2 text-[12.8px] leading-none '>

View file

@ -3,9 +3,8 @@
import Pagination from '../../Pagination.svelte'
import { Button } from '../button'
import { Profile } from '../profile'
import * as Avatar from '$lib/components/ui/avatar'
import * as Tooltip from '$lib/components/ui/tooltip'
import { client, type Media } from '$lib/modules/anilist'
import { isMobile, since } from '$lib/utils'
@ -76,17 +75,9 @@
</div>
<div class='flex w-full justify-between mt-auto text-[9.6px]'>
<div class='pt-2 flex items-end'>
<Tooltip.Root>
<Tooltip.Trigger class='inline-block size-4 cursor-default mr-2'>
<Avatar.Root class='inline-block size-4'>
<Avatar.Image src={thread.user?.avatar?.medium ?? ''} alt={thread.user?.name ?? 'N/A'} />
<Avatar.Fallback>{thread.user?.name ?? 'N/A'}</Avatar.Fallback>
</Avatar.Root>
</Tooltip.Trigger>
<Tooltip.Content>
<p class='font-extrabold'>{thread.user?.name}</p>
</Tooltip.Content>
</Tooltip.Root>
{#if thread.user}
<Profile user={thread.user} class='size-4 mr-2' />
{/if}
{since(new Date(thread.createdAt * 1000))}
</div>
<div class='ml-auto inline-flex flex-wrap gap-2 items-end'>

View file

@ -0,0 +1,107 @@
<script lang='ts'>
import type { UserFrag } from '$lib/modules/anilist/queries'
import type { ResultOf } from 'gql.tada'
import type { HTMLAttributes } from 'svelte/elements'
import Shadow from '$lib/components/Shadow.svelte'
import * as Avatar from '$lib/components/ui/avatar'
import * as Popover from '$lib/components/ui/popover'
import { cn, since } from '$lib/utils'
type $$Props = HTMLAttributes<HTMLImageElement> & {
user: ResultOf<typeof UserFrag>
}
let className: $$Props['class'] = 'inline-block ring-4 ring-black size-8 bg-black'
export { className as class }
export let user: ResultOf<typeof UserFrag>
</script>
{#if user}
{@const name = user.name}
{@const avatar = user.avatar?.medium ?? ''}
{@const banner = user.bannerImage ?? ''}
{@const bubble = user.donatorBadge}
<div class='flex'>
<Popover.Root>
<Popover.Trigger class='flex'>
<Avatar.Root class={className}>
<Avatar.Image src={avatar} alt={name} />
<Avatar.Fallback>{name}</Avatar.Fallback>
</Avatar.Root>
</Popover.Trigger>
<Popover.Content class='p-1 m-3 rounded-md shadow root-bg border-none w-auto' style='--theme-base-color: {user.options?.profileColor ?? '#000'}'>
<div class='w-[300px] rounded core-bg gap-2 flex flex-col pb-2'>
<div class={cn('w-full h-[105px] relative p-3 flex items-end', !banner && 'bg-white/10')}>
{#if banner}
<img src={banner} alt='banner' class='absolute top-0 left-0 w-full h-full rounded-t opacity-50 pointer-events-none object-cover' />
{/if}
<Avatar.Root class='inline-block size-20'>
<Avatar.Image src={avatar} alt={name} />
<Avatar.Fallback>{name}</Avatar.Fallback>
</Avatar.Root>
<div class='min-w-0 flex flex-col pl-3 relative'>
<div class='font-extrabold pb-0.5 text-2xl text-ellipsis overflow-clip pr-0.5'>
{name}
</div>
<div class='details text-neutral-200 flex text-[11px]'>
{#if user.isFollower}
<span class='text-nowrap flex items-center'>Follows you</span>
{/if}
<span class='text-nowrap flex items-center'>Joined {since(new Date((user.createdAt ?? 0) * 1000))}</span>
</div>
</div>
{#if bubble && bubble !== 'Donator'}
<div class='-left-5 -top-11 absolute text-sm'>
<div class='px-4 py-2 rounded-2xl bg-mix bubbles relative leading-tight'>
<span class='text-contrast'>
{bubble}
</span>
</div>
</div>
{/if}
</div>
<Shadow html={user.about ?? 'No user description'} class='w-full max-h-[200px] text-sm py-2 px-4 overflow-y-auto overflow-x-clip' />
<div class='details text-neutral-200 flex text-[11px] px-4'>
<span class='text-nowrap flex items-center'>{user.statistics?.anime?.count ?? 0} anime</span>
<span class='text-nowrap flex items-center'>{user.statistics?.anime?.episodesWatched ?? 0} episodes</span>
<span class='text-nowrap flex items-center'>{since(new Date(Date.now() - (user.statistics?.anime?.minutesWatched ?? 0) * 60 * 1000)).replace('ago', 'watched')}</span>
</div>
</div>
</Popover.Content>
</Popover.Root>
</div>
{/if}
<style>
:global(.root-bg) {
background: linear-gradient(var(--theme-base-color, #000), rgb(34, 33, 30))
}
.core-bg {
background: color-mix(in oklab, #141414 30%, var(--theme-base-color, #000) 100%);
}
.bg-mix {
background: color-mix(in oklab, #fff 100%, var(--theme-base-color, #000) 50%);
}
.bubbles::before {
top: 42px;
left: 10px;
height: 20px;
width: 20px;
background: inherit;
content: '';
position: absolute;
border-radius: 50%;
}
.bubbles::after {
top: 70px;
left: 20px;
height: 10px;
width: 10px;
background: inherit;
content: '';
position: absolute;
border-radius: 50%;
}
</style>

View file

@ -0,0 +1 @@
export { default as Profile } from './Profile.svelte'

View file

@ -219,7 +219,8 @@ class AnilistClient {
AiringSchedule: () => null,
MediaListCollection: e => (e.user as {id: string | null}).id,
MediaListGroup: () => null,
UserAvatar: () => null
UserAvatar: () => null,
UserOptions: () => null
}
}),
authExchange(async utils => {

View file

@ -24,30 +24,30 @@ declare module 'gql.tada' {
TadaDocumentNode<{ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; }, {}, { fragment: "ScheduleMedia"; on: "Media"; masked: false; }>;
"\n query Schedule($seasonCurrent: MediaSeason, $seasonYearCurrent: Int, $seasonLast: MediaSeason, $seasonYearLast: Int, $seasonNext: MediaSeason, $seasonYearNext: Int, $ids: [Int]) {\n curr1: Page(page: 1) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n curr2: Page(page: 2) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n curr3: Page(page: 3) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n residue: Page(page: 1) {\n media(type: ANIME, season: $seasonLast, seasonYear: $seasonYearLast, episodes_greater: 16, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n },\n next1: Page(page: 1) {\n media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n },\n next2: Page(page: 2) {\n media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n }\n":
TadaDocumentNode<{ curr1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr3: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; residue: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; }, { ids?: (number | null)[] | null | undefined; seasonYearNext?: number | null | undefined; seasonNext?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearLast?: number | null | undefined; seasonLast?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearCurrent?: number | null | undefined; seasonCurrent?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; }, void>;
"\n query Following($id: Int!) {\n Page {\n mediaList(mediaId: $id, isFollowing: true, sort: UPDATED_TIME_DESC) {\n id,\n status,\n score,\n progress,\n user {\n id,\n name,\n avatar {\n medium\n }\n }\n }\n }\n }\n":
TadaDocumentNode<{ Page: { mediaList: ({ id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; score: number | null; progress: number | null; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; } | null)[] | null; } | null; }, { id: number; }, void>;
"\n mutation Entry($lists: [String], $id: Int!, $status: MediaListStatus, $progress: Int, $repeat: Int, $score: Int) {\n SaveMediaListEntry(mediaId: $id, status: $status, progress: $progress, repeat: $repeat, scoreRaw: $score, customLists: $lists) {\n id,\n media {\n id,\n status,\n mediaListEntry {\n ...FullMediaList\n },\n nextAiringEpisode {\n episode\n }\n }\n }\n }\n":
TadaDocumentNode<{ SaveMediaListEntry: { id: number; media: { id: number; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; mediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; } | null; nextAiringEpisode: { episode: number; } | null; } | null; } | null; }, { score?: number | null | undefined; repeat?: number | null | undefined; progress?: number | null | undefined; status?: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null | undefined; id: number; lists?: (string | null)[] | null | undefined; }, void>;
"\n fragment UserFrag on User @_unmask {\n id,\n bannerImage,\n about,\n isFollowing,\n isFollower,\n donatorBadge,\n options {\n profileColor\n },\n createdAt,\n name,\n avatar {\n medium\n },\n statistics {\n anime {\n count,\n minutesWatched,\n episodesWatched,\n genres(limit: 3, sort: COUNT_DESC) {\n genre,\n count\n }\n }\n }\n }\n":
TadaDocumentNode<{ id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { medium: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; }, {}, { fragment: "UserFrag"; on: "User"; masked: false; }>;
"\n query Following($id: Int!) {\n Page {\n mediaList(mediaId: $id, isFollowing: true, sort: UPDATED_TIME_DESC) {\n id,\n status,\n score,\n progress,\n user {\n ...UserFrag\n }\n }\n }\n }\n":
TadaDocumentNode<{ Page: { mediaList: ({ id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; score: number | null; progress: number | null; user: { id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { medium: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; } | null)[] | null; } | null; }, { id: number; }, void>;
"\n mutation Entry($lists: [String], $id: Int!, $status: MediaListStatus, $progress: Int, $repeat: Int, $score: Int) {\n SaveMediaListEntry(mediaId: $id, status: $status, progress: $progress, repeat: $repeat, scoreRaw: $score, customLists: $lists) {\n id,\n ...FullMediaList,\n media {\n id\n }\n }\n }\n":
TadaDocumentNode<{ SaveMediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; media: { id: number; } | null; } | null; }, { score?: number | null | undefined; repeat?: number | null | undefined; progress?: number | null | undefined; status?: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null | undefined; id: number; lists?: (string | null)[] | null | undefined; }, void>;
"\n mutation DeleteEntry($id: Int!) {\n DeleteMediaListEntry(id: $id) {\n deleted\n }\n }\n":
TadaDocumentNode<{ DeleteMediaListEntry: { deleted: boolean | null; } | null; }, { id: number; }, void>;
"\n mutation ToggleFavourite($id: Int!) {\n ToggleFavourite(animeId: $id) { anime { nodes { id } } } \n }\n":
TadaDocumentNode<{ ToggleFavourite: { anime: { nodes: ({ id: number; } | null)[] | null; } | null; } | null; }, { id: number; }, void>;
"\n fragment ThreadFrag on Thread @_unmask {\n id,\n title,\n body(asHtml: true),\n userId,\n replyCount,\n viewCount,\n isLocked,\n isSubscribed,\n isLiked,\n likeCount,\n repliedAt,\n createdAt,\n user {\n id,\n name,\n avatar {\n medium\n }\n },\n categories {\n id,\n name\n }\n }\n":
TadaDocumentNode<{ id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; }, {}, { fragment: "ThreadFrag"; on: "Thread"; masked: false; }>;
"\n fragment ThreadFrag on Thread @_unmask {\n id,\n title,\n body,\n userId,\n replyCount,\n viewCount,\n isLocked,\n isSubscribed,\n isLiked,\n likeCount,\n repliedAt,\n createdAt,\n user {\n ...UserFrag\n },\n categories {\n id,\n name\n }\n }\n":
TadaDocumentNode<{ id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { medium: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; }, {}, { fragment: "ThreadFrag"; on: "Thread"; masked: false; }>;
"\n query Threads($id: Int!, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n hasNextPage\n },\n threads(mediaCategoryId: $id, sort: ID_DESC) {\n ...ThreadFrag\n }\n }\n }\n":
TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; threads: ({ id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; } | null)[] | null; } | null; }, { perPage?: number | null | undefined; page?: number | null | undefined; id: number; }, void>;
TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; threads: ({ id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { medium: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; } | null)[] | null; } | null; }, { perPage?: number | null | undefined; page?: number | null | undefined; id: number; }, void>;
"\n query Thread($threadId: Int!) {\n Thread(id: $threadId) {\n ...ThreadFrag\n }\n }\n":
TadaDocumentNode<{ Thread: { id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; } | null; }, { threadId: number; }, void>;
"\n fragment CommentFrag on ThreadComment @_unmask {\n id,\n comment(asHtml: true),\n isLiked,\n likeCount,\n createdAt,\n user {\n id,\n name,\n moderatorRoles,\n avatar {\n medium\n },\n },\n childComments{\n id\n },\n isLocked\n }\n":
TadaDocumentNode<{ id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; name: string; moderatorRoles: ("ADMIN" | "LEAD_DEVELOPER" | "DEVELOPER" | "LEAD_COMMUNITY" | "COMMUNITY" | "DISCORD_COMMUNITY" | "LEAD_ANIME_DATA" | "ANIME_DATA" | "LEAD_MANGA_DATA" | "MANGA_DATA" | "LEAD_SOCIAL_MEDIA" | "SOCIAL_MEDIA" | "RETIRED" | "CHARACTER_DATA" | "STAFF_DATA" | null)[] | null; avatar: { medium: string | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; }, {}, { fragment: "CommentFrag"; on: "ThreadComment"; masked: false; }>;
"\n query Comments($threadId: Int, $page: Int) {\n Page(page: $page, perPage: 15) {\n pageInfo {\n hasNextPage\n }\n threadComments(threadId: $threadId) {\n id,\n comment(asHtml: true),\n isLiked,\n likeCount,\n createdAt,\n user {\n id,\n name,\n moderatorRoles,\n avatar {\n medium\n },\n },\n childComments{\n id\n },\n isLocked\n }\n }\n }\n":
TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; threadComments: ({ id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; name: string; moderatorRoles: ("ADMIN" | "LEAD_DEVELOPER" | "DEVELOPER" | "LEAD_COMMUNITY" | "COMMUNITY" | "DISCORD_COMMUNITY" | "LEAD_ANIME_DATA" | "ANIME_DATA" | "LEAD_MANGA_DATA" | "MANGA_DATA" | "LEAD_SOCIAL_MEDIA" | "SOCIAL_MEDIA" | "RETIRED" | "CHARACTER_DATA" | "STAFF_DATA" | null)[] | null; avatar: { medium: string | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; } | null)[] | null; } | null; }, { page?: number | null | undefined; threadId?: number | null | undefined; }, void>;
"\n query User ($id: Int) {\n User(id: $id) {\n id,\n name,\n avatar {\n large\n },\n bannerImage,\n about(asHtml: true),\n isFollowing,\n isFollower,\n donatorBadge,\n createdAt,\n options {\n profileColor\n },\n statistics {\n anime {\n count,\n minutesWatched,\n episodesWatched,\n genrePreview: genres(limit: 10, sort: COUNT_DESC) {\n genre,\n count\n }\n }\n }\n }\n }\n":
TadaDocumentNode<{ User: { id: number; name: string; avatar: { large: string | null; } | null; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; createdAt: number | null; options: { profileColor: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genrePreview: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; }, { id?: number | null | undefined; }, void>;
TadaDocumentNode<{ Thread: { id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { medium: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; } | null; }, { threadId: number; }, void>;
"\n fragment CommentFrag on ThreadComment @_unmask {\n id,\n comment,\n isLiked,\n likeCount,\n createdAt,\n user {\n ...UserFrag\n },\n childComments,\n isLocked\n }\n":
TadaDocumentNode<{ id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { medium: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; }, {}, { fragment: "CommentFrag"; on: "ThreadComment"; masked: false; }>;
"\n query Comments($threadId: Int, $page: Int) {\n Page(page: $page, perPage: 15) {\n pageInfo {\n hasNextPage\n }\n threadComments(threadId: $threadId) {\n id,\n comment,\n isLiked,\n likeCount,\n createdAt,\n user {\n id,\n name,\n avatar {\n medium\n },\n },\n childComments,\n isLocked\n }\n }\n }\n":
TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; threadComments: ({ id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; } | null)[] | null; } | null; }, { page?: number | null | undefined; threadId?: number | null | undefined; }, void>;
"\n mutation ToggleLike ($id: Int!, $type: LikeableType!) {\n ToggleLikeV2(id: $id, type: $type) {\n ... on Thread {\n id\n likeCount\n isLiked\n }\n ... on ThreadComment {\n id\n likeCount\n isLiked\n }\n }\n }\n":
TadaDocumentNode<{ ToggleLikeV2: { __typename?: "ActivityReply" | undefined; } | { __typename?: "ListActivity" | undefined; } | { __typename?: "MessageActivity" | undefined; } | { __typename?: "TextActivity" | undefined; } | { __typename?: "Thread" | undefined; id: number; likeCount: number; isLiked: boolean | null; } | { __typename?: "ThreadComment" | undefined; id: number; likeCount: number; isLiked: boolean | null; } | null; }, { type: "THREAD" | "THREAD_COMMENT" | "ACTIVITY" | "ACTIVITY_REPLY"; id: number; }, void>;
"\n mutation SaveThreadComment ($id: Int, $threadId: Int, $parentCommentId: Int, $comment: String) {\n SaveThreadComment(id: $id, threadId: $threadId, parentCommentId: $parentCommentId, comment: $comment) {\n ...CommentFrag\n }\n }\n":
TadaDocumentNode<{ SaveThreadComment: { id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; name: string; moderatorRoles: ("ADMIN" | "LEAD_DEVELOPER" | "DEVELOPER" | "LEAD_COMMUNITY" | "COMMUNITY" | "DISCORD_COMMUNITY" | "LEAD_ANIME_DATA" | "ANIME_DATA" | "LEAD_MANGA_DATA" | "MANGA_DATA" | "LEAD_SOCIAL_MEDIA" | "SOCIAL_MEDIA" | "RETIRED" | "CHARACTER_DATA" | "STAFF_DATA" | null)[] | null; avatar: { medium: string | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; } | null; }, { comment?: string | null | undefined; parentCommentId?: number | null | undefined; threadId?: number | null | undefined; id?: number | null | undefined; }, void>;
TadaDocumentNode<{ SaveThreadComment: { id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { medium: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; } | null; }, { comment?: string | null | undefined; parentCommentId?: number | null | undefined; threadId?: number | null | undefined; id?: number | null | undefined; }, void>;
"\n mutation DeleteThreadComment ($id: Int) {\n DeleteThreadComment(id: $id) {\n deleted\n }\n }\n":
TadaDocumentNode<{ DeleteThreadComment: { deleted: boolean | null; } | null; }, { id?: number | null | undefined; }, void>;
"fragment Med on Media {id, isFavourite}":

View file

@ -250,6 +250,36 @@ export const Schedule = gql(`
}
`, [ScheduleMedia])
export const UserFrag = gql(`
fragment UserFrag on User @_unmask {
id,
bannerImage,
about,
isFollowing,
isFollower,
donatorBadge,
options {
profileColor
},
createdAt,
name,
avatar {
medium
},
statistics {
anime {
count,
minutesWatched,
episodesWatched,
genres(limit: 3, sort: COUNT_DESC) {
genre,
count
}
}
}
}
`)
export const Following = gql(`
query Following($id: Int!) {
Page {
@ -259,16 +289,12 @@ export const Following = gql(`
score,
progress,
user {
id,
name,
avatar {
medium
}
...UserFrag
}
}
}
}
`)
`, [UserFrag])
// AL API is dog, fullmedialist is NULL when queried inside media..., it's possible this can cause cache loops, but there's no other way to do this!!!
export const Entry = gql(`
@ -312,18 +338,14 @@ export const ThreadFrag = gql(`
repliedAt,
createdAt,
user {
id,
name,
avatar {
medium
}
...UserFrag
},
categories {
id,
name
}
}
`)
`, [UserFrag])
export const Threads = gql(`
query Threads($id: Int!, $page: Int, $perPage: Int) {
@ -354,17 +376,12 @@ export const CommentFrag = gql(`
likeCount,
createdAt,
user {
id,
name,
moderatorRoles,
avatar {
medium
},
...UserFrag
},
childComments,
isLocked
}
`)
`, [UserFrag])
// AL in their infinite wisdom decided to make childComments infer the schema of the parent comment, so we can't use the CommentFrag here
export const Comments = gql(`
@ -380,51 +397,14 @@ export const Comments = gql(`
likeCount,
createdAt,
user {
id,
name,
moderatorRoles,
avatar {
medium
},
...UserFrag
},
childComments,
isLocked
}
}
}
`)
export const User = gql(`
query User ($id: Int) {
User(id: $id) {
id,
name,
avatar {
large
},
bannerImage,
about,
isFollowing,
isFollower,
donatorBadge,
createdAt,
options {
profileColor
},
statistics {
anime {
count,
minutesWatched,
episodesWatched,
genrePreview: genres(limit: 10, sort: COUNT_DESC) {
genre,
count
}
}
}
}
}
`)
`, [UserFrag])
export const ToggleLike = gql(`
mutation ToggleLike ($id: Int!, $type: LikeableType!) {

View file

@ -26,6 +26,7 @@ for (const { pointer, value } of pointerTypes) {
const noop: () => void = () => undefined
// this is for nested click elements, its svelte's |preventDefault for other components
export function clickwrap (cb: (_: MouseEvent) => unknown = noop) {
return (e: MouseEvent) => {
e.stopPropagation()

View file

@ -8,12 +8,11 @@
import Anilist from '$lib/components/icons/Anilist.svelte'
import MyAnimeList from '$lib/components/icons/MyAnimeList.svelte'
import * as Avatar from '$lib/components/ui/avatar'
import { bannerSrc, hideBanner } from '$lib/components/ui/banner'
import { PlayButton, Button, BookmarkButton, FavoriteButton } from '$lib/components/ui/button'
import * as Dialog from '$lib/components/ui/dialog'
import { Load } from '$lib/components/ui/img'
import * as Tooltip from '$lib/components/ui/tooltip'
import { Profile } from '$lib/components/ui/profile'
import { cover, desc, duration, format, season, status, title } from '$lib/modules/anilist'
import { authAggregator, of } from '$lib/modules/auth'
import native from '$lib/modules/native'
@ -142,18 +141,9 @@
</Button>
<div class='-space-x-1 md:ml-3 hidden md:flex'>
{#each followerEntries as followerEntry, i (followerEntry?.user?.id ?? i)}
<Tooltip.Root>
<Tooltip.Trigger class='inline-block size-8 cursor-default'>
<Avatar.Root class='inline-block ring-4 ring-black size-8 bg-black'>
<Avatar.Image src={followerEntry?.user?.avatar?.medium ?? ''} alt={followerEntry?.user?.name ?? 'N/A'} />
<Avatar.Fallback>{followerEntry?.user?.name ?? 'N/A'}</Avatar.Fallback>
</Avatar.Root>
</Tooltip.Trigger>
<Tooltip.Content>
<p class='font-extrabold'>{followerEntry?.user?.name}</p>
<p class='capitalize'>{followerEntry?.status?.toLowerCase()}</p>
</Tooltip.Content>
</Tooltip.Root>
{#if followerEntry?.user}
<Profile user={followerEntry.user} />
{/if}
{/each}
</div>
</div>

View file

@ -4,9 +4,9 @@
import type { PageData } from './$types'
import Shadow from '$lib/components/Shadow.svelte'
import * as Avatar from '$lib/components/ui/avatar'
import { Button, iconSizes } from '$lib/components/ui/button'
import { Comments, Write } from '$lib/components/ui/forums'
import { Profile } from '$lib/components/ui/profile'
import { client } from '$lib/modules/anilist'
import { since } from '$lib/utils'
@ -46,11 +46,10 @@
<div class='rounded-md bg-neutral-950 text-secondary-foreground flex w-full mb-10 py-6 px-8 flex-col'>
<div class='flex w-full justify-between text-xl'>
<div class='font-bold mb-2 line-clamp-1 flex leading-none items-center'>
<Avatar.Root class='inline-block size-8 mr-4'>
<Avatar.Image src={thread.user?.avatar?.medium ?? ''} alt={thread.user?.name ?? 'N/A'} />
<Avatar.Fallback>{thread.user?.name ?? 'N/A'}</Avatar.Fallback>
</Avatar.Root>
{thread.user?.name ?? 'N/A'}
{#if thread.user}
<Profile user={thread.user} class='size-8 mr-4' />
{thread.user.name ?? 'N/A'}
{/if}
</div>
<div class='flex ml-2 text-[12.8px] leading-none mt-0.5'>
<Heart size='12' class='mr-1' fill={thread.isLiked ? 'currentColor' : 'transparent'} />