Coves frontend - a photon fork
1<script lang="ts">
2 import { goto } from '$app/navigation'
3 import { client, site } from '$lib/api/client.svelte'
4 import { profile } from '$lib/app/auth.svelte'
5 import { errorMessage } from '$lib/app/error'
6 import { t } from '$lib/app/i18n'
7 import MarkdownEditor from '$lib/app/markdown/MarkdownEditor.svelte'
8 import ImageInputUpload from '$lib/ui/form/ImageInputUpload.svelte'
9 import { Header } from '$lib/ui/layout'
10 import {
11 Badge,
12 Button,
13 Label,
14 Material,
15 Menu,
16 MenuButton,
17 Option,
18 Select,
19 Switch,
20 TextInput,
21 toast,
22 } from 'mono-svelte'
23 import { GlobeAlt, Icon, MapPin, Plus } from 'svelte-hero-icons/dist'
24 import { addSubscription } from '../user'
25
26 interface Props {
27 /**
28 * The community ID to edit.
29 */
30 edit?: number
31 formData?: {
32 name: string
33 displayName: string
34 icon?: string
35 banner?: string
36 sidebar?: string
37 nsfw: boolean
38 postsLockedToModerators: boolean
39 submitting: boolean
40 visibility: 'Public' | 'LocalOnly'
41 languages?: number[]
42 }
43 formtitle?: import('svelte').Snippet
44 }
45
46 let {
47 edit = undefined,
48 formData: passedFormData = $bindable({
49 name: '',
50 displayName: '',
51 sidebar: '',
52 nsfw: false,
53 postsLockedToModerators: false,
54 submitting: false,
55 visibility: 'Public',
56 languages: undefined,
57 }),
58 formtitle,
59 }: Props = $props()
60
61 let formData = $state(passedFormData)
62
63 async function submit() {
64 if (!profile.current?.jwt) return
65 if ((!edit && formData.name == '') || formData.displayName == '') return
66
67 formData.submitting = true
68
69 try {
70 const res = edit
71 ? await client().editCommunity({
72 title: formData.displayName,
73 description: formData.sidebar,
74 nsfw: formData.nsfw,
75 posting_restricted_to_mods: formData.postsLockedToModerators,
76 icon: formData.icon,
77 banner: formData.banner,
78 community_id: edit,
79 visibility: formData.visibility,
80 discussion_languages: formData.languages,
81 })
82 : await client().createCommunity({
83 name: formData.name,
84 title: formData.displayName,
85 description: formData.sidebar,
86 nsfw: formData.nsfw,
87 posting_restricted_to_mods: formData.postsLockedToModerators,
88 icon: formData.icon,
89 banner: formData.banner,
90 visibility: formData.visibility,
91 discussion_languages: formData.languages,
92 })
93
94 toast({
95 content: $t('toast.updatedCommunity'),
96 type: 'success',
97 })
98
99 // TODO: Update local state when Coves API provides user moderates data
100 addSubscription(res.community_view.community, true)
101
102 if (!edit)
103 goto(
104 `/c/${res.community_view.community.name}@${
105 new URL(res.community_view.community.actor_id).hostname
106 }`,
107 )
108 } catch (err) {
109 toast({
110 content: errorMessage(err as string),
111 type: 'error',
112 })
113 }
114
115 formData.submitting = false
116 }
117</script>
118
119<form
120 onsubmit={(e) => {
121 e.preventDefault()
122 submit()
123 }}
124 class="flex flex-col gap-4 h-full w-full"
125>
126 {#if formtitle}{@render formtitle()}{:else}
127 <Header>{$t('form.post.community')}</Header>
128 {/if}
129 <TextInput
130 required
131 label={$t('form.name')}
132 bind:value={formData.name}
133 oninput={() => {
134 formData.name = formData.name.toLowerCase().replaceAll(' ', '_')
135 }}
136 disabled={edit != undefined}
137 />
138 <TextInput
139 required
140 label={$t('form.profile.displayName')}
141 bind:value={formData.displayName}
142 />
143 <div class="flex flex-row gap-4 flex-wrap *:flex-1">
144 <ImageInputUpload
145 bind:imageUrl={formData.icon}
146 label={$t('routes.admin.config.icon')}
147 />
148 <ImageInputUpload
149 bind:imageUrl={formData.banner}
150 label={$t('routes.admin.config.banner')}
151 />
152 </div>
153 <MarkdownEditor
154 previewButton
155 label={$t('routes.admin.config.sidebar')}
156 bind:value={formData.sidebar}
157 />
158
159 <Switch bind:checked={formData.nsfw}>{$t('post.badges.nsfw')}</Switch>
160 <Switch bind:checked={formData.postsLockedToModerators}>
161 Only moderators can post
162 </Switch>
163 <Select label="Visibility" class="w-max" bind:value={formData.visibility}>
164 <Option icon={GlobeAlt} value="Public">Public</Option>
165 <Option icon={MapPin} value="LocalOnly">Local Only</Option>
166 </Select>
167
168 <div class="space-y-1">
169 <Label>{$t('form.profile.languages.title')}</Label>
170 <Material rounding="xl" color="uniform" class="dark:bg-zinc-950">
171 {#if site.data}
172 <div class="flex gap-2 flex-wrap flex-row">
173 <Menu class="gap-px">
174 {#snippet target(attachment)}
175 <button {@attach attachment} type="button">
176 <Badge color="blue-subtle">
177 <Icon src={Plus} micro size="14" />
178 {$t('common.add')}
179 </Badge>
180 </button>
181 {/snippet}
182 {#each site.data.all_languages.filter((l) => !formData.languages?.includes(l.id)) as language (language.id)}
183 <MenuButton
184 class="min-h-[16px] py-0"
185 onclick={() => {
186 formData.languages = [
187 ...(formData.languages ?? []),
188 language.id,
189 ]
190 }}
191 >
192 {language.name}
193 </MenuButton>
194 {/each}
195 </Menu>
196 {#each formData.languages ?? [] as languageId, index (languageId)}
197 {@const language = site.data.all_languages.find(
198 (l) => l.id == languageId,
199 )}
200 <button
201 type="button"
202 class="hover:brightness-150 transition-all"
203 onclick={() => {
204 formData.languages?.splice(index, 1)
205 }}
206 >
207 <Badge class="cursor-pointer">{language?.name}</Badge>
208 </button>
209 {/each}
210 </div>
211 {/if}
212 </Material>
213 </div>
214
215 <Button
216 submit
217 color="primary"
218 size="lg"
219 class="mt-auto"
220 loading={formData.submitting}
221 disabled={formData.submitting}
222 >
223 {edit ? $t('common.save') : $t('form.submit')}
224 </Button>
225</form>