Coves frontend - a photon fork
1<script lang="ts">
2 import type { PostView } from '$lib/api/coves/types'
3 import { type View, settings } from '$lib/app/settings.svelte'
4 import { publishedToDate } from '$lib/ui/util/date'
5 import type { ClassValue } from 'svelte/elements'
6 import {
7 PostActions,
8 PostBody,
9 PostMedia,
10 PostMediaCompact,
11 PostMeta,
12 } from '.'
13 import { extractEmbedTitle, extractEmbedUrl, mediaType } from './helpers'
14 import { type MetaTag, parseTags } from './PostMeta.svelte'
15
16 interface Props {
17 post: PostView
18 actions?: boolean
19 hideCommunity?: boolean
20 pinned?: boolean
21 view?: View
22 style?: string
23 class?: ClassValue
24 extraBadges?: import('svelte').Snippet
25 }
26
27 let {
28 post = $bindable(),
29 actions = true,
30 hideCommunity = false,
31 pinned = false,
32 view = settings.view,
33 style = '',
34 class: clazz = '',
35 extraBadges,
36 }: Props = $props()
37
38 let tags = $derived.by<{ title?: string; tags: MetaTag[] }>(() => {
39 const parsed = parseTags(post.record?.title)
40
41 return {
42 title: parsed.title,
43 tags: parsed.tags,
44 }
45 })
46 let type = $derived(mediaType(post.embed))
47 let embedUrl = $derived(extractEmbedUrl(post.embed))
48 let embedTitle = $derived(extractEmbedTitle(post.embed))
49 let hideTitle = $derived(
50 settings.posts.deduplicateEmbed &&
51 embedTitle === post.record?.title &&
52 view !== 'compact' &&
53 type !== 'iframe',
54 )
55
56 let badges = $derived({
57 featured: pinned,
58 saved: post.viewer?.saved ?? false,
59 })
60</script>
61
62<!--
63 @component
64 This is the sole component for displaying posts.
65 It adapts to all kinds of form factors for different contexts, such as feeds, full post view, and crosspost list.
66-->
67<article
68 class={[
69 'relative group/post',
70 settings.leftAlign && 'left-align',
71 view == 'compact' && 'py-3 list-type compact',
72 view == 'cozy' && 'py-5 flex flex-col gap-2',
73 clazz,
74 ]}
75 id={post.uri}
76 {style}
77>
78 <PostMeta
79 community={post.community}
80 showCommunity={!hideCommunity}
81 user={post.author}
82 published={publishedToDate(post.createdAt)}
83 {badges}
84 uri={post.uri}
85 title={hideTitle
86 ? undefined
87 : tags?.title
88 ? tags.title
89 : post.record?.title}
90 style="grid-area: meta;"
91 edited={post.editedAt}
92 tags={tags?.tags}
93 postUrl={embedUrl}
94 {view}
95 {extraBadges}
96 />
97 {#key embedUrl}
98 <div style="grid-area:embed;" class={{ contents: view == 'cozy' }}>
99 <PostMedia embed={post.embed} {view} {type} />
100 </div>
101 {#if view == 'compact'}
102 <PostMediaCompact
103 embed={post.embed}
104 {type}
105 class="{settings.leftAlign ? 'mr-3' : 'ml-3'} shrink no-list-margin"
106 style="grid-area: media;"
107 {view}
108 />
109 {/if}
110 {/key}
111 {#if post.record?.content && view != 'compact'}
112 <PostBody
113 element="section"
114 body={post.record.content}
115 style="grid-area: body"
116 class="relative"
117 />
118 {/if}
119 {#if actions}
120 <PostActions bind:post style="grid-area: actions;" {view} />
121 {/if}
122</article>
123
124<style>
125 .list-type {
126 display: grid;
127 grid-template-areas: 'meta media' 'title media' 'body media' 'embed embed' 'actions actions';
128 grid-template-columns: minmax(0, 1fr) auto;
129 width: 100%;
130 height: 100%;
131 }
132
133 /* Swap media/item positions */
134 .list-type.left-align {
135 grid-template-areas: 'media meta' 'media title' 'media body' 'embed embed' 'actions actions';
136 grid-template-columns: auto minmax(0, 1fr);
137 }
138
139 /* Has media on the right for all of them */
140 @media (min-width: 480px) {
141 .list-type.compact {
142 grid-template-areas: 'meta media' 'title media' 'body media' 'embed media' 'actions media';
143 }
144 }
145
146 /* Swap above again */
147 @media (min-width: 480px) {
148 .list-type.compact.left-align {
149 grid-template-areas: 'media meta' 'media title' 'media body' 'media embed' 'media actions';
150 }
151 }
152
153 :global(.compact > *:not(.no-list-margin):not(:first-child)) {
154 margin-top: 0.3rem;
155 }
156
157 :global(.list-type:not(.compact) > *:not(.no-list-margin):not(:first-child)) {
158 margin-top: 0.5rem;
159 }
160</style>