mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-03-11 22:15:35 +00:00
feat: profile view component
This commit is contained in:
parent
34f9a196ea
commit
ef4303810f
15 changed files with 193 additions and 131 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 '>
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
107
src/lib/components/ui/profile/Profile.svelte
Normal file
107
src/lib/components/ui/profile/Profile.svelte
Normal 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>
|
||||
1
src/lib/components/ui/profile/index.ts
Normal file
1
src/lib/components/ui/profile/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Profile } from './Profile.svelte'
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
30
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
30
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
|
|
@ -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}":
|
||||
|
|
|
|||
|
|
@ -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!) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue