/ src / lib / components / project-customizer / project-customizer.svelte
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>