Thomas G. Lopes commited on
Commit
9104321
·
1 Parent(s): cf47645

add option to paste in project

Browse files
src/lib/components/local-toasts.svelte CHANGED
@@ -17,20 +17,21 @@
17
 
18
  type Toast = {
19
  content: string;
 
20
  id: string;
21
  };
22
 
23
  let toasts = $state<Toast[]>([]);
24
  let timeouts: ReturnType<typeof window.setTimeout>[] = [];
25
 
26
- function addToast(content: string) {
27
  const id = crypto.randomUUID();
28
  const timeout = setTimeout(() => {
29
  toasts = toasts.filter(t => t.id !== id);
30
  timeouts = timeouts.filter(t => t !== timeout);
31
  }, closeDelay);
32
 
33
- toasts.push({ content, id });
34
  timeouts.push(timeout);
35
  }
36
 
@@ -57,13 +58,21 @@
57
  destroy: autoUpdate(triggerEl, node, compute),
58
  };
59
  }
 
 
 
 
 
 
60
  </script>
61
 
62
  {@render children({ trigger, addToast })}
63
 
64
  {#each toasts as toast (toast.id)}
65
  <div
66
- class="rounded-full border border-blue-400 bg-gradient-to-b from-blue-500 to-blue-600 px-2 py-1 text-xs"
 
 
67
  in:fly={{ y: 10 }}
68
  out:fly={{ y: -4 }}
69
  use:float
@@ -73,7 +82,7 @@
73
  {/each}
74
 
75
  <style>
76
- div {
77
  /* Float on top of the UI */
78
  position: absolute;
79
 
 
17
 
18
  type Toast = {
19
  content: string;
20
+ variant: "info" | "danger";
21
  id: string;
22
  };
23
 
24
  let toasts = $state<Toast[]>([]);
25
  let timeouts: ReturnType<typeof window.setTimeout>[] = [];
26
 
27
+ function addToast(content: string, variant?: Toast["variant"]) {
28
  const id = crypto.randomUUID();
29
  const timeout = setTimeout(() => {
30
  toasts = toasts.filter(t => t.id !== id);
31
  timeouts = timeouts.filter(t => t !== timeout);
32
  }, closeDelay);
33
 
34
+ toasts.push({ content, id, variant: variant ?? "info" });
35
  timeouts.push(timeout);
36
  }
37
 
 
58
  destroy: autoUpdate(triggerEl, node, compute),
59
  };
60
  }
61
+
62
+ const classMap: Record<Toast["variant"], string> = {
63
+ info: "border border-blue-400 bg-gradient-to-b from-blue-500 to-blue-600",
64
+
65
+ danger: "border border-red-400 bg-gradient-to-b from-red-500 to-red-600",
66
+ };
67
  </script>
68
 
69
  {@render children({ trigger, addToast })}
70
 
71
  {#each toasts as toast (toast.id)}
72
  <div
73
+ data-local-toast
74
+ data-variant={toast.variant}
75
+ class="rounded-full px-2 py-1 text-xs {classMap[toast.variant]}"
76
  in:fly={{ y: 10 }}
77
  out:fly={{ y: -4 }}
78
  use:float
 
82
  {/each}
83
 
84
  <style>
85
+ [data-local-toast] {
86
  /* Float on top of the UI */
87
  position: absolute;
88
 
src/lib/components/share-modal.svelte CHANGED
@@ -14,30 +14,34 @@
14
  import { clickOutside } from "$lib/actions/click-outside.js";
15
  import { session } from "$lib/state/session.svelte";
16
  import type { Project } from "$lib/types.js";
17
- import { onMount } from "svelte";
 
18
  import { fade, scale } from "svelte/transition";
 
19
  import IconCross from "~icons/carbon/close";
20
  import IconCopy from "~icons/carbon/copy";
 
21
  import LocalToasts from "./local-toasts.svelte";
22
- import { encodeObject } from "$lib/utils/encode.js";
23
- import { copyToClipboard } from "$lib/utils/copy.js";
24
- let dialog: HTMLDialogElement | undefined = $state();
25
 
26
- onMount(() => {
27
- // JUST FOR DEVELOPMENT
28
- project = session.project;
29
- });
30
 
31
  const open = $derived(!!project);
32
  const encoded = $derived(encodeObject(project));
 
33
 
34
  $effect(() => {
35
  if (open) {
36
  dialog?.showModal();
37
  } else {
38
- setTimeout(() => dialog?.close(), 250);
 
 
 
39
  }
40
  });
 
 
41
  </script>
42
 
43
  <dialog class="backdrop:bg-transparent" bind:this={dialog} onclose={() => close()}>
@@ -93,6 +97,47 @@
93
  {/snippet}
94
  </LocalToasts>
95
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </div>
97
 
98
  <!-- Modal footer -->
 
14
  import { clickOutside } from "$lib/actions/click-outside.js";
15
  import { session } from "$lib/state/session.svelte";
16
  import type { Project } from "$lib/types.js";
17
+ import { copyToClipboard } from "$lib/utils/copy.js";
18
+ import { decodeString, encodeObject } from "$lib/utils/encode.js";
19
  import { fade, scale } from "svelte/transition";
20
+ import typia from "typia";
21
  import IconCross from "~icons/carbon/close";
22
  import IconCopy from "~icons/carbon/copy";
23
+ import IconSave from "~icons/carbon/save";
24
  import LocalToasts from "./local-toasts.svelte";
25
+ import { addToast as addToastGlobally } from "./toaster.svelte.js";
 
 
26
 
27
+ let dialog: HTMLDialogElement | undefined = $state();
 
 
 
28
 
29
  const open = $derived(!!project);
30
  const encoded = $derived(encodeObject(project));
31
+ let pasted = $state("");
32
 
33
  $effect(() => {
34
  if (open) {
35
  dialog?.showModal();
36
  } else {
37
+ setTimeout(() => {
38
+ dialog?.close();
39
+ pasted = "";
40
+ }, 250);
41
  }
42
  });
43
+
44
+ const isProject = typia.createIs<Project>();
45
  </script>
46
 
47
  <dialog class="backdrop:bg-transparent" bind:this={dialog} onclose={() => close()}>
 
97
  {/snippet}
98
  </LocalToasts>
99
  </div>
100
+
101
+ <div class="my-4 flex items-center gap-2">
102
+ <div class="h-px grow bg-neutral-500" aria-hidden="true"></div>
103
+ <span class="text-xs text-neutral-400">or</span>
104
+ <div class="h-px grow bg-neutral-500" aria-hidden="true"></div>
105
+ </div>
106
+
107
+ <h3 class="text-lg font-semibold">Save a copied project</h3>
108
+ <p>Paste a copied project string, and save it for your local usage.</p>
109
+ <LocalToasts>
110
+ {#snippet children({ addToast, trigger })}
111
+ <form
112
+ class="mt-4 flex gap-2"
113
+ onsubmit={e => {
114
+ e.preventDefault();
115
+ const decoded = decodeString(pasted);
116
+ if (!isProject(decoded)) {
117
+ addToast("String isn't valid", "danger");
118
+ return;
119
+ }
120
+ session.addProject({ ...decoded, name: `Saved - ${decoded.name}`, id: crypto.randomUUID() });
121
+ addToastGlobally({
122
+ variant: "success",
123
+ title: "Saved project",
124
+ description: "The project you pasted in was successfully saved.",
125
+ });
126
+ close();
127
+ }}
128
+ >
129
+ <input
130
+ class="grow rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
131
+ type="text"
132
+ bind:value={pasted}
133
+ />
134
+ <button {...trigger} class="btn flex items-center gap-2" type="submit">
135
+ <IconSave />
136
+ Save
137
+ </button>
138
+ </form>
139
+ {/snippet}
140
+ </LocalToasts>
141
  </div>
142
 
143
  <!-- Modal footer -->
src/lib/state/session.svelte.ts CHANGED
@@ -150,6 +150,10 @@ class SessionState {
150
 
151
  defaultProject.conversations = [getDefaults().defaultConversation];
152
 
 
 
 
 
153
  this.$ = { ...this.$, projects: [...this.$.projects, project], activeProjectId: project.id };
154
  };
155
 
 
150
 
151
  defaultProject.conversations = [getDefaults().defaultConversation];
152
 
153
+ this.addProject(project);
154
+ };
155
+
156
+ addProject = (project: Project) => {
157
  this.$ = { ...this.$, projects: [...this.$.projects, project], activeProjectId: project.id };
158
  };
159
 
src/lib/utils/copy.ts CHANGED
@@ -10,8 +10,7 @@ export async function copyToClipboard(text: string): Promise<void> {
10
  try {
11
  await navigator.clipboard.writeText(text);
12
  return; // Resolve immediately if successful
13
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
- } catch (_) {
15
  // Fallback to the older method
16
  }
17
  }
 
10
  try {
11
  await navigator.clipboard.writeText(text);
12
  return; // Resolve immediately if successful
13
+ } catch {
 
14
  // Fallback to the older method
15
  }
16
  }
src/lib/utils/encode.ts CHANGED
@@ -23,7 +23,11 @@ export function decodeString(encodedString: string): unknown {
23
  * Returns:
24
  * The decoded object.
25
  */
26
- const jsonString: string = decodeURIComponent(escape(atob(encodedString)));
27
- const obj: unknown = JSON.parse(jsonString);
28
- return obj;
 
 
 
 
29
  }
 
23
  * Returns:
24
  * The decoded object.
25
  */
26
+ try {
27
+ const jsonString: string = decodeURIComponent(escape(atob(encodedString)));
28
+ const obj: unknown = JSON.parse(jsonString);
29
+ return obj;
30
+ } catch {
31
+ return null;
32
+ }
33
  }