project-customizer.svelte
1 <script lang="ts" module> 2 import { PROJECT_PROFILE_HEADER_FRAGMENT } from '../project-profile-header/project-profile-header.svelte'; 3 4 export const PROJECT_CUSTOMIZER_FRAGMENT = gql` 5 ${PROJECT_PROFILE_HEADER_FRAGMENT} 6 fragment ProjectCustomizer on Project { 7 ...ProjectProfileHeader 8 chainData { 9 ... on ClaimedProjectData { 10 avatar { 11 ... on EmojiAvatar { 12 emoji 13 } 14 ... on ImageAvatar { 15 cid 16 } 17 } 18 color 19 } 20 } 21 isVisible 22 } 23 `; 24 </script> 25 26 <script lang="ts"> 27 import { run } from 'svelte/legacy'; 28 29 import type { Writable } from 'svelte/store'; 30 import FormField from '../form-field/form-field.svelte'; 31 import { gql } from 'graphql-request'; 32 import type { ProjectCustomizerFragment } from './__generated__/gql.generated'; 33 import FileUpload from '../custom-avatar-upload/custom-avatar-upload.svelte'; 34 import TabbedBox from '../tabbed-box/tabbed-box.svelte'; 35 import filterCurrentChainData from '$lib/utils/filter-current-chain-data'; 36 import Toggle from '$lib/components/toggle/toggle.svelte'; 37 import EmojiPicker from '../emoji-picker/emoji-picker.svelte'; 38 import ColorPicker from '../color-picker/color-picker.svelte'; 39 40 interface Props { 41 newProjectData: Writable< 42 ReturnType< 43 typeof filterCurrentChainData<ProjectCustomizerFragment['chainData'][number], 'claimed'> 44 > & { isProjectVisible: boolean } 45 >; 46 valid?: boolean; 47 } 48 49 let { newProjectData, valid = $bindable(false) }: Props = $props(); 50 51 let activeTab: 'tab1' | 'tab2' = $state( 52 $newProjectData.avatar.__typename === 'EmojiAvatar' ? 'tab1' : 'tab2', 53 ); 54 55 let selectedEmoji = $state( 56 $newProjectData.avatar.__typename === 'EmojiAvatar' ? $newProjectData.avatar.emoji : '💧', 57 ); 58 function handleEmojiChange(newEmoji: string) { 59 $newProjectData.avatar = { 60 __typename: 'EmojiAvatar', 61 emoji: newEmoji, 62 }; 63 } 64 run(() => { 65 handleEmojiChange(selectedEmoji); 66 }); 67 68 let selectedColor = $state($newProjectData.color); 69 function handleColorChange(newColor: string) { 70 $newProjectData.color = newColor; 71 } 72 run(() => { 73 handleColorChange(selectedColor); 74 }); 75 76 let isVisible = $state($newProjectData.isProjectVisible); 77 function handleIsVisibleChange(isVisible: boolean) { 78 $newProjectData.isProjectVisible = isVisible; 79 } 80 run(() => { 81 handleIsVisibleChange(isVisible); 82 }); 83 84 let lastUploadedCid = $state( 85 $newProjectData.avatar.__typename === 'ImageAvatar' ? $newProjectData.avatar.cid : undefined, 86 ); 87 function handleFileUpload(e: CustomEvent<{ ipfsCid: string }>) { 88 if (activeTab !== 'tab2') { 89 return; 90 } 91 92 lastUploadedCid = e.detail.ipfsCid; 93 94 $newProjectData.avatar = { 95 __typename: 'ImageAvatar', 96 cid: lastUploadedCid, 97 }; 98 } 99 100 function handleTabChange(newTab: 'tab1' | 'tab2', lastUploadedCid: string | undefined) { 101 if (newTab === 'tab1') { 102 $newProjectData.avatar = { 103 __typename: 'EmojiAvatar', 104 emoji: selectedEmoji, 105 }; 106 } else if (newTab === 'tab2' && lastUploadedCid) { 107 $newProjectData = { 108 ...$newProjectData, 109 avatar: { 110 __typename: 'ImageAvatar', 111 cid: lastUploadedCid, 112 }, 113 }; 114 } else { 115 return; 116 } 117 } 118 run(() => { 119 handleTabChange(activeTab, lastUploadedCid); 120 }); 121 122 run(() => { 123 valid = Boolean(activeTab === 'tab1' || lastUploadedCid); 124 }); 125 </script> 126 127 <div class="project-customizer"> 128 <TabbedBox bind:activeTab ariaLabel="Avatar settings" border={true}> 129 {#snippet tab1()} 130 <EmojiPicker bind:selectedEmoji /> 131 {/snippet} 132 {#snippet tab2()} 133 <FileUpload on:uploaded={handleFileUpload} /> 134 {/snippet} 135 </TabbedBox> 136 137 <FormField type="div" title="Color"> 138 <ColorPicker bind:selectedColor /> 139 </FormField> 140 141 <FormField type="div" title="Visibility"> 142 <div class="visibility-toggle"> 143 <div style="display: flex; gap: 0.5rem; "> 144 <p>Show this project on my profile</p> 145 <a 146 style="text-decoration: underline; display: inline;" 147 target="_blank" 148 href="https://docs.drips.network/advanced/drip-list-and-project-visibility" 149 > 150 Learn more 151 </a> 152 </div> 153 <Toggle bind:checked={isVisible} /> 154 </div> 155 </FormField> 156 </div> 157 158 <style> 159 .project-customizer { 160 display: flex; 161 gap: 1.5rem; 162 flex-direction: column; 163 } 164 165 .visibility-toggle { 166 display: flex; 167 justify-content: space-between; 168 } 169 </style>