;
8 | }
9 | interface PageData {
10 | user: import('@supabase/supabase-js').User | null;
11 | }
12 | }
13 | }
14 |
15 | export {};
16 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
16 | %sveltekit.head%
17 | 3db
18 |
19 |
20 | %sveltekit.body%
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
2 | import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit';
3 | import type { Handle } from '@sveltejs/kit';
4 |
5 | export const handle: Handle = async ({ event, resolve }) => {
6 | event.locals.supabase = createSupabaseServerClient({
7 | supabaseUrl: PUBLIC_SUPABASE_URL,
8 | supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
9 | event
10 | });
11 |
12 | event.locals.getUser = async () => {
13 | const {
14 | data: { session }
15 | } = await event.locals.supabase.auth.getSession();
16 |
17 | if (!session) {
18 | return null;
19 | }
20 |
21 | const {
22 | data: { user },
23 | error
24 | } = await event.locals.supabase.auth.getUser();
25 |
26 | if (error) {
27 | console.error('Error fetching user:', error);
28 | return null;
29 | }
30 |
31 | return user;
32 | };
33 |
34 | return resolve(event, {
35 | filterSerializedResponseHeaders(name) {
36 | return name === 'content-range';
37 | }
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/src/lib/components/create-repo-dialog.svelte:
--------------------------------------------------------------------------------
1 |
100 |
101 |
102 |
103 |
104 | Link or Create Repository
105 | Create a new GitHub repository to use as a database.
106 |
107 |
108 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/src/lib/components/file-browser.svelte:
--------------------------------------------------------------------------------
1 |
131 |
132 |
133 | {#if !$currentRepository}
134 |
135 |
136 | Toggle Sidebar
137 |
138 | {/if}
139 |
140 | {#if $currentRepository}
141 |
142 |
143 |
144 |
145 | Toggle Sidebar
146 |
147 |
148 | {#each getBreadcrumbs() as crumb}
149 | loadContents(crumb.path)}>
150 | {crumb.name}
151 |
152 | {#if crumb.path !== getBreadcrumbs().slice(-1)[0].path}
153 | /
154 | {/if}
155 | {/each}
156 |
157 |
158 |
159 | {#if loading}
160 |
Loading...
161 | {:else if error}
162 |
163 | {error}
164 |
165 | {:else if contents.length === 0}
166 |
This folder is empty
167 | {:else}
168 |
169 |
170 |
171 |
172 | Name
173 | Size
174 | Actions
175 |
176 |
177 |
178 | {#each contents.toSorted((a, b) => {
179 | if (a.type === 'dir' && b.type !== 'dir') return -1;
180 | if (b.type === 'dir' && a.type !== 'dir') return 1;
181 | if (a.name === 'index.json' && $currentRepository?.name === '3db-service') return 1;
182 | if (b.name === 'index.json' && $currentRepository?.name === '3db-service') return -1;
183 | return a.name.localeCompare(b.name);
184 | }) as item}
185 |
186 |
187 | handleItemClick(item)}>
188 | {#if item.type === 'dir'}
189 |
190 | {:else}
191 |
192 | {/if}
193 | {item.name}
194 | {#if item.type === 'dir'}
195 |
199 | {:else}
200 |
204 | {/if}
205 |
206 |
207 |
208 | {item.type === 'dir' ? '--' : formatBytes(item.size)}
209 |
210 |
211 | {#if item.type === 'file' || item.type === 'dir'}
212 |
213 | {#if item.type === 'file'}
214 | handleDownload(item)}
219 | >
220 |
221 |
222 | handleCopyUrl(item)}
227 | >
228 |
231 |
232 | {/if}
233 | handleDelete(item)}
238 | >
239 |
240 |
241 |
242 | {/if}
243 |
244 |
245 | {/each}
246 |
247 |
248 |
249 | {/if}
250 | {:else}
251 |
252 | Select a repository to view its contents
253 |
254 | {/if}
255 |
256 |
--------------------------------------------------------------------------------
/src/lib/components/file-upload-dialog.svelte:
--------------------------------------------------------------------------------
1 |
87 |
88 |
89 |
90 |
91 | Upload File
92 | Upload a file to the current directory.
93 |
94 |
95 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/src/lib/components/github-app-prompt.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 | Install 3db
16 |
17 | To use 3db, you need to install our GitHub App. This will allow us to manage your
18 | repositories.
19 |
20 |
21 |
22 |
23 |
24 |
25 |
The app will only use these permissions:
26 |
27 |
28 |
29 | Read and write access to repositories
30 |
31 |
32 |
33 | Create and delete repositories
34 |
35 |
36 |
37 | Note: A repo called '3db-service' will be created. It holds the metadata for connected
38 | repos.
39 |
40 |
41 |
(window.location.href = installationUrl)} class="w-fit">
42 |
43 | Install GitHub App
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/lib/components/repo-context-menu.svelte:
--------------------------------------------------------------------------------
1 |
77 |
78 |
79 |
80 |
81 | {@render children?.()}
82 |
83 |
84 |
85 |
86 |
87 | Open on GitHub
88 |
89 |
90 |
91 |
92 | Disconnect Repository
93 |
94 |
95 |
96 | Delete Repository
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/lib/components/ui/button/button.svelte:
--------------------------------------------------------------------------------
1 |
39 |
40 |
54 |
55 | {#if href}
56 |
57 | {@render children?.()}
58 |
59 | {:else}
60 |
66 | {@render children?.()}
67 |
68 | {/if}
69 |
--------------------------------------------------------------------------------
/src/lib/components/ui/button/index.ts:
--------------------------------------------------------------------------------
1 | import Root, {
2 | type ButtonProps,
3 | type ButtonSize,
4 | type ButtonVariant,
5 | buttonVariants
6 | } from './button.svelte';
7 |
8 | export {
9 | Root,
10 | type ButtonProps as Props,
11 | //
12 | Root as Button,
13 | buttonVariants,
14 | type ButtonProps,
15 | type ButtonSize,
16 | type ButtonVariant
17 | };
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-content.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | {@render children?.()}
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-description.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | {@render children?.()}
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-footer.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | {@render children?.()}
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-header.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | {@render children?.()}
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-title.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
24 | {@render children?.()}
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
19 | {@render children?.()}
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './card.svelte';
2 | import Content from './card-content.svelte';
3 | import Description from './card-description.svelte';
4 | import Footer from './card-footer.svelte';
5 | import Header from './card-header.svelte';
6 | import Title from './card-title.svelte';
7 |
8 | export {
9 | Root,
10 | Content,
11 | Description,
12 | Footer,
13 | Header,
14 | Title,
15 | //
16 | Root as Card,
17 | Content as CardContent,
18 | Description as CardDescription,
19 | Footer as CardFooter,
20 | Header as CardHeader,
21 | Title as CardTitle
22 | };
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
30 | {#snippet children({ checked, indeterminate })}
31 |
32 | {#if indeterminate}
33 |
34 | {:else}
35 |
36 | {/if}
37 |
38 | {@render childrenProp?.()}
39 | {/snippet}
40 |
41 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-content.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-group-heading.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-item.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-radio-item.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
22 | {#snippet children({ checked })}
23 |
24 | {#if checked}
25 |
26 | {/if}
27 |
28 | {@render childrenProp?.({ checked })}
29 | {/snippet}
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-separator.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-shortcut.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
19 | {@render children?.()}
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-sub-content.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
26 | {@render children?.()}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/lib/components/ui/context-menu/index.ts:
--------------------------------------------------------------------------------
1 | import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
2 |
3 | import Item from './context-menu-item.svelte';
4 | import GroupHeading from './context-menu-group-heading.svelte';
5 | import Content from './context-menu-content.svelte';
6 | import Shortcut from './context-menu-shortcut.svelte';
7 | import RadioItem from './context-menu-radio-item.svelte';
8 | import Separator from './context-menu-separator.svelte';
9 | import SubContent from './context-menu-sub-content.svelte';
10 | import SubTrigger from './context-menu-sub-trigger.svelte';
11 | import CheckboxItem from './context-menu-checkbox-item.svelte';
12 |
13 | const Sub = ContextMenuPrimitive.Sub;
14 | const Root = ContextMenuPrimitive.Root;
15 | const Trigger = ContextMenuPrimitive.Trigger;
16 | const Group = ContextMenuPrimitive.Group;
17 | const RadioGroup = ContextMenuPrimitive.RadioGroup;
18 |
19 | export {
20 | Sub,
21 | Root,
22 | Item,
23 | GroupHeading,
24 | Group,
25 | Trigger,
26 | Content,
27 | Shortcut,
28 | Separator,
29 | RadioItem,
30 | SubContent,
31 | SubTrigger,
32 | RadioGroup,
33 | CheckboxItem,
34 | //
35 | Root as ContextMenu,
36 | Sub as ContextMenuSub,
37 | Item as ContextMenuItem,
38 | GroupHeading as ContextMenuGroupHeading,
39 | Group as ContextMenuGroup,
40 | Content as ContextMenuContent,
41 | Trigger as ContextMenuTrigger,
42 | Shortcut as ContextMenuShortcut,
43 | RadioItem as ContextMenuRadioItem,
44 | Separator as ContextMenuSeparator,
45 | RadioGroup as ContextMenuRadioGroup,
46 | SubContent as ContextMenuSubContent,
47 | SubTrigger as ContextMenuSubTrigger,
48 | CheckboxItem as ContextMenuCheckboxItem
49 | };
50 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-content.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
28 | {@render children?.()}
29 |
32 |
33 | Close
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-description.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-footer.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
19 | {@render children?.()}
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-header.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
19 | {@render children?.()}
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-overlay.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-title.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/index.ts:
--------------------------------------------------------------------------------
1 | import { Dialog as DialogPrimitive } from 'bits-ui';
2 |
3 | import Title from './dialog-title.svelte';
4 | import Footer from './dialog-footer.svelte';
5 | import Header from './dialog-header.svelte';
6 | import Overlay from './dialog-overlay.svelte';
7 | import Content from './dialog-content.svelte';
8 | import Description from './dialog-description.svelte';
9 |
10 | const Root = DialogPrimitive.Root;
11 | const Trigger = DialogPrimitive.Trigger;
12 | const Close = DialogPrimitive.Close;
13 | const Portal = DialogPrimitive.Portal;
14 |
15 | export {
16 | Root,
17 | Title,
18 | Portal,
19 | Footer,
20 | Header,
21 | Trigger,
22 | Overlay,
23 | Content,
24 | Description,
25 | Close,
26 | //
27 | Root as Dialog,
28 | Title as DialogTitle,
29 | Portal as DialogPortal,
30 | Footer as DialogFooter,
31 | Header as DialogHeader,
32 | Trigger as DialogTrigger,
33 | Overlay as DialogOverlay,
34 | Content as DialogContent,
35 | Description as DialogDescription,
36 | Close as DialogClose
37 | };
38 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
30 | {#snippet children({ checked, indeterminate })}
31 |
32 | {#if indeterminate}
33 |
34 | {:else}
35 |
36 | {/if}
37 |
38 | {@render childrenProp?.()}
39 | {/snippet}
40 |
41 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
22 | {@render children?.()}
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
22 | {#snippet children({ checked })}
23 |
24 | {#if checked}
25 |
26 | {/if}
27 |
28 | {@render childrenProp?.({ checked })}
29 | {/snippet}
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
19 | {@render children?.()}
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
26 | {@render children?.()}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/index.ts:
--------------------------------------------------------------------------------
1 | import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
2 | import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
3 | import Content from './dropdown-menu-content.svelte';
4 | import GroupHeading from './dropdown-menu-group-heading.svelte';
5 | import Item from './dropdown-menu-item.svelte';
6 | import Label from './dropdown-menu-label.svelte';
7 | import RadioItem from './dropdown-menu-radio-item.svelte';
8 | import Separator from './dropdown-menu-separator.svelte';
9 | import Shortcut from './dropdown-menu-shortcut.svelte';
10 | import SubContent from './dropdown-menu-sub-content.svelte';
11 | import SubTrigger from './dropdown-menu-sub-trigger.svelte';
12 |
13 | const Sub = DropdownMenuPrimitive.Sub;
14 | const Root = DropdownMenuPrimitive.Root;
15 | const Trigger = DropdownMenuPrimitive.Trigger;
16 | const Group = DropdownMenuPrimitive.Group;
17 | const RadioGroup = DropdownMenuPrimitive.RadioGroup;
18 |
19 | export {
20 | CheckboxItem,
21 | Content,
22 | Root as DropdownMenu,
23 | CheckboxItem as DropdownMenuCheckboxItem,
24 | Content as DropdownMenuContent,
25 | Group as DropdownMenuGroup,
26 | GroupHeading as DropdownMenuGroupHeading,
27 | Item as DropdownMenuItem,
28 | Label as DropdownMenuLabel,
29 | RadioGroup as DropdownMenuRadioGroup,
30 | RadioItem as DropdownMenuRadioItem,
31 | Separator as DropdownMenuSeparator,
32 | Shortcut as DropdownMenuShortcut,
33 | Sub as DropdownMenuSub,
34 | SubContent as DropdownMenuSubContent,
35 | SubTrigger as DropdownMenuSubTrigger,
36 | Trigger as DropdownMenuTrigger,
37 | Group,
38 | GroupHeading,
39 | Item,
40 | Label,
41 | RadioGroup,
42 | RadioItem,
43 | Root,
44 | Separator,
45 | Shortcut,
46 | Sub,
47 | SubContent,
48 | SubTrigger,
49 | Trigger
50 | };
51 |
--------------------------------------------------------------------------------
/src/lib/components/ui/input/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './input.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Input
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/input/input.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/separator/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './separator.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Separator
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/separator/separator.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/index.ts:
--------------------------------------------------------------------------------
1 | import { Dialog as SheetPrimitive } from 'bits-ui';
2 | import Overlay from './sheet-overlay.svelte';
3 | import Content from './sheet-content.svelte';
4 | import Header from './sheet-header.svelte';
5 | import Footer from './sheet-footer.svelte';
6 | import Title from './sheet-title.svelte';
7 | import Description from './sheet-description.svelte';
8 |
9 | const Root = SheetPrimitive.Root;
10 | const Close = SheetPrimitive.Close;
11 | const Trigger = SheetPrimitive.Trigger;
12 | const Portal = SheetPrimitive.Portal;
13 |
14 | export {
15 | Root,
16 | Close,
17 | Trigger,
18 | Portal,
19 | Overlay,
20 | Content,
21 | Header,
22 | Footer,
23 | Title,
24 | Description,
25 | //
26 | Root as Sheet,
27 | Close as SheetClose,
28 | Trigger as SheetTrigger,
29 | Portal as SheetPortal,
30 | Overlay as SheetOverlay,
31 | Content as SheetContent,
32 | Header as SheetHeader,
33 | Footer as SheetFooter,
34 | Title as SheetTitle,
35 | Description as SheetDescription
36 | };
37 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-content.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
41 |
42 |
43 |
44 |
45 | {@render children?.()}
46 |
49 |
50 | Close
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-description.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-footer.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
19 | {@render children?.()}
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-header.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
19 | {@render children?.()}
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-overlay.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-title.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/constants.ts:
--------------------------------------------------------------------------------
1 | export const SIDEBAR_COOKIE_NAME = 'sidebar:state';
2 | export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
3 | export const SIDEBAR_WIDTH = '16rem';
4 | export const SIDEBAR_WIDTH_MOBILE = '18rem';
5 | export const SIDEBAR_WIDTH_ICON = '3rem';
6 | export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
7 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/context.svelte.ts:
--------------------------------------------------------------------------------
1 | import { IsMobile } from '$lib/hooks/is-mobile.svelte.js';
2 | import { getContext, setContext } from 'svelte';
3 | import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js';
4 |
5 | type Getter = () => T;
6 |
7 | export type SidebarStateProps = {
8 | /**
9 | * A getter function that returns the current open state of the sidebar.
10 | * We use a getter function here to support `bind:open` on the `Sidebar.Provider`
11 | * component.
12 | */
13 | open: Getter;
14 |
15 | /**
16 | * A function that sets the open state of the sidebar. To support `bind:open`, we need
17 | * a source of truth for changing the open state to ensure it will be synced throughout
18 | * the sub-components and any `bind:` references.
19 | */
20 | setOpen: (open: boolean) => void;
21 | };
22 |
23 | class SidebarState {
24 | readonly props: SidebarStateProps;
25 | open = $derived.by(() => this.props.open());
26 | openMobile = $state(false);
27 | setOpen: SidebarStateProps['setOpen'];
28 | #isMobile: IsMobile;
29 | state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
30 |
31 | constructor(props: SidebarStateProps) {
32 | this.setOpen = props.setOpen;
33 | this.#isMobile = new IsMobile();
34 | this.props = props;
35 | }
36 |
37 | // Convenience getter for checking if the sidebar is mobile
38 | // without this, we would need to use `sidebar.isMobile.current` everywhere
39 | get isMobile() {
40 | return this.#isMobile.current;
41 | }
42 |
43 | // Event handler to apply to the ``
44 | handleShortcutKeydown = (e: KeyboardEvent) => {
45 | if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
46 | e.preventDefault();
47 | this.toggle();
48 | }
49 | };
50 |
51 | setOpenMobile = (value: boolean) => {
52 | this.openMobile = value;
53 | };
54 |
55 | toggle = () => {
56 | return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
57 | };
58 | }
59 |
60 | const SYMBOL_KEY = 'scn-sidebar';
61 |
62 | /**
63 | * Instantiates a new `SidebarState` instance and sets it in the context.
64 | *
65 | * @param props The constructor props for the `SidebarState` class.
66 | * @returns The `SidebarState` instance.
67 | */
68 | export function setSidebar(props: SidebarStateProps): SidebarState {
69 | return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
70 | }
71 |
72 | /**
73 | * Retrieves the `SidebarState` instance from the context. This is a class instance,
74 | * so you cannot destructure it.
75 | * @returns The `SidebarState` instance.
76 | */
77 | export function useSidebar(): SidebarState {
78 | return getContext(Symbol.for(SYMBOL_KEY));
79 | }
80 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/index.ts:
--------------------------------------------------------------------------------
1 | import { useSidebar } from './context.svelte.js';
2 | import Content from './sidebar-content.svelte';
3 | import Footer from './sidebar-footer.svelte';
4 | import GroupAction from './sidebar-group-action.svelte';
5 | import GroupContent from './sidebar-group-content.svelte';
6 | import GroupLabel from './sidebar-group-label.svelte';
7 | import Group from './sidebar-group.svelte';
8 | import Header from './sidebar-header.svelte';
9 | import Input from './sidebar-input.svelte';
10 | import Inset from './sidebar-inset.svelte';
11 | import MenuAction from './sidebar-menu-action.svelte';
12 | import MenuBadge from './sidebar-menu-badge.svelte';
13 | import MenuButton from './sidebar-menu-button.svelte';
14 | import MenuItem from './sidebar-menu-item.svelte';
15 | import MenuSkeleton from './sidebar-menu-skeleton.svelte';
16 | import MenuSubButton from './sidebar-menu-sub-button.svelte';
17 | import MenuSubItem from './sidebar-menu-sub-item.svelte';
18 | import MenuSub from './sidebar-menu-sub.svelte';
19 | import Menu from './sidebar-menu.svelte';
20 | import Provider from './sidebar-provider.svelte';
21 | import Rail from './sidebar-rail.svelte';
22 | import Separator from './sidebar-separator.svelte';
23 | import Trigger from './sidebar-trigger.svelte';
24 | import Root from './sidebar.svelte';
25 |
26 | export {
27 | Content,
28 | Footer,
29 | Group,
30 | GroupAction,
31 | GroupContent,
32 | GroupLabel,
33 | Header,
34 | Input,
35 | Inset,
36 | Menu,
37 | MenuAction,
38 | MenuBadge,
39 | MenuButton,
40 | MenuItem,
41 | MenuSkeleton,
42 | MenuSub,
43 | MenuSubButton,
44 | MenuSubItem,
45 | Provider,
46 | Rail,
47 | Root,
48 | Separator,
49 | //
50 | Root as Sidebar,
51 | Content as SidebarContent,
52 | Footer as SidebarFooter,
53 | Group as SidebarGroup,
54 | GroupAction as SidebarGroupAction,
55 | GroupContent as SidebarGroupContent,
56 | GroupLabel as SidebarGroupLabel,
57 | Header as SidebarHeader,
58 | Input as SidebarInput,
59 | Inset as SidebarInset,
60 | Menu as SidebarMenu,
61 | MenuAction as SidebarMenuAction,
62 | MenuBadge as SidebarMenuBadge,
63 | MenuButton as SidebarMenuButton,
64 | MenuItem as SidebarMenuItem,
65 | MenuSkeleton as SidebarMenuSkeleton,
66 | MenuSub as SidebarMenuSub,
67 | MenuSubButton as SidebarMenuSubButton,
68 | MenuSubItem as SidebarMenuSubItem,
69 | Provider as SidebarProvider,
70 | Rail as SidebarRail,
71 | Separator as SidebarSeparator,
72 | Trigger as SidebarTrigger,
73 | Trigger,
74 | useSidebar
75 | };
76 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-content.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
23 | {@render children?.()}
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-footer.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
20 | {@render children?.()}
21 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-group-action.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 | {#if child}
31 | {@render child({ props: propObj })}
32 | {:else}
33 |
34 | {@render children?.()}
35 |
36 | {/if}
37 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-group-content.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
20 | {@render children?.()}
21 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-group-label.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 | {#if child}
29 | {@render child({ props: mergedProps })}
30 | {:else}
31 |
32 | {@render children?.()}
33 |
34 | {/if}
35 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-group.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
20 | {@render children?.()}
21 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-header.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
20 | {@render children?.()}
21 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-input.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-inset.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
23 | {@render children?.()}
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-action.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 | {#if child}
38 | {@render child({ props: mergedProps })}
39 | {:else}
40 |
41 | {@render children?.()}
42 |
43 | {/if}
44 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
28 | {@render children?.()}
29 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-button.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
66 |
67 | {#snippet Button({ props }: { props?: Record })}
68 | {@const mergedProps = mergeProps(buttonProps, props)}
69 | {#if child}
70 | {@render child({ props: mergedProps })}
71 | {:else}
72 |
73 | {@render children?.()}
74 |
75 | {/if}
76 | {/snippet}
77 |
78 | {#if !tooltipContent}
79 | {@render Button({})}
80 | {:else}
81 |
82 |
83 | {#snippet child({ props })}
84 | {@render Button({ props })}
85 | {/snippet}
86 |
87 |
94 |
95 | {/if}
96 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-item.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
27 | {#if showIcon}
28 |
29 | {/if}
30 |
35 | {@render children?.()}
36 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 | {#if child}
38 | {@render child({ props: mergedProps })}
39 | {:else}
40 |
41 | {@render children?.()}
42 |
43 | {/if}
44 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | {@render children?.()}
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
24 | {@render children?.()}
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
20 | {@render children?.()}
21 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-provider.svelte:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
48 |
57 | {@render children?.()}
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-rail.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 | sidebar.toggle()}
23 | title="Toggle Sidebar"
24 | class={cn(
25 | 'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
26 | '[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
27 | '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
28 | 'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
29 | '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
30 | '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
31 | className
32 | )}
33 | {...restProps}
34 | >
35 | {@render children?.()}
36 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-separator.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-trigger.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 | {
23 | onclick?.(e);
24 | sidebar.toggle();
25 | }}
26 | data-sidebar="trigger"
27 | variant="ghost"
28 | size="icon"
29 | class={cn('h-7 w-7', className)}
30 | {...restProps}
31 | >
32 |
33 | Toggle Sidebar
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 | {#if collapsible === 'none'}
27 |
35 | {@render children?.()}
36 |
37 | {:else if sidebar.isMobile}
38 |
44 |
55 |
56 | {:else}
57 |
98 | {/if}
99 |
--------------------------------------------------------------------------------
/src/lib/components/ui/skeleton/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './skeleton.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Skeleton
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/skeleton/skeleton.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tooltip/index.ts:
--------------------------------------------------------------------------------
1 | import { Tooltip as TooltipPrimitive } from 'bits-ui';
2 | import Content from './tooltip-content.svelte';
3 |
4 | const Root = TooltipPrimitive.Root;
5 | const Trigger = TooltipPrimitive.Trigger;
6 | const Provider = TooltipPrimitive.Provider;
7 |
8 | export {
9 | Root,
10 | Trigger,
11 | Content,
12 | Provider,
13 | //
14 | Root as Tooltip,
15 | Content as TooltipContent,
16 | Trigger as TooltipTrigger,
17 | Provider as TooltipProvider
18 | };
19 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tooltip/tooltip-content.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/user-menu.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 |
27 |
{user?.user_metadata.name}
28 |
{user?.email}
29 |
30 |
35 |
36 |
37 |
38 |
39 |
40 | 3db source code
41 |
42 |
43 |
44 |
45 | Open My GitHub Repos
46 |
47 | theme.toggle()}>
48 |
49 | Toggle Theme
50 |
51 |
52 |
53 |
54 | Sign Out
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/lib/hooks/is-mobile.svelte.ts:
--------------------------------------------------------------------------------
1 | import { untrack } from 'svelte';
2 |
3 | const MOBILE_BREAKPOINT = 768;
4 |
5 | export class IsMobile {
6 | #current = $state(false);
7 |
8 | constructor() {
9 | $effect(() => {
10 | return untrack(() => {
11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12 | const onChange = () => {
13 | this.#current = window.innerWidth < MOBILE_BREAKPOINT;
14 | };
15 | mql.addEventListener('change', onChange);
16 | onChange();
17 | return () => {
18 | mql.removeEventListener('change', onChange);
19 | };
20 | });
21 | });
22 | }
23 |
24 | get current() {
25 | return this.#current;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/services/github.ts:
--------------------------------------------------------------------------------
1 | import type { FileContent, Repository } from '$lib/types';
2 |
3 | export type GitHubConfig = {
4 | token: string;
5 | userEmail: string;
6 | };
7 |
8 | const createCommitter = (userEmail: string) => ({
9 | name: 'db3 service',
10 | email: userEmail
11 | });
12 |
13 | async function request(
14 | config: GitHubConfig,
15 | endpoint: string,
16 | options: RequestInit = {}
17 | ): Promise {
18 | const response = await fetch(`https://api.github.com${endpoint}`, {
19 | ...options,
20 | headers: {
21 | Authorization: `token ${config.token}`,
22 | Accept: 'application/vnd.github.v3+json',
23 | ...options.headers
24 | }
25 | });
26 |
27 | if (!response.ok) {
28 | throw new Error(`GitHub API error: ${response.statusText}`);
29 | }
30 |
31 | return response.json();
32 | }
33 |
34 | export async function createRepository(config: GitHubConfig, name: string): Promise {
35 | return request(config, '/user/repos', {
36 | method: 'POST',
37 | body: JSON.stringify({
38 | name,
39 | private: false,
40 | auto_init: true
41 | })
42 | });
43 | }
44 |
45 | export async function getRepositories(config: GitHubConfig): Promise {
46 | let allRepos: Repository[] = [];
47 | let nextUrl = '/user/repos?per_page=100&sort=created';
48 |
49 | while (nextUrl) {
50 | const response = await fetch(`https://api.github.com${nextUrl}`, {
51 | headers: {
52 | Authorization: `token ${config.token}`,
53 | Accept: 'application/vnd.github.v3+json'
54 | }
55 | });
56 |
57 | if (!response.ok) {
58 | throw new Error(`GitHub API error: ${response.statusText}`);
59 | }
60 |
61 | const repos = await response.json();
62 | allRepos = [...allRepos, ...repos];
63 |
64 | // Check for Link header and extract next URL
65 | const linkHeader = response.headers.get('link');
66 | if (!linkHeader) break;
67 |
68 | const links = linkHeader.split(',');
69 | const nextLink = links.find((link) => link.includes('rel="next"'));
70 | if (!nextLink) break;
71 |
72 | // Extract URL from next link
73 | const matches = nextLink.match(/<([^>]+)>/);
74 | nextUrl = matches ? matches[1].replace('https://api.github.com', '') : '';
75 | }
76 |
77 | return allRepos;
78 | }
79 |
80 | export async function getContents(
81 | config: GitHubConfig,
82 | owner: string,
83 | repo: string,
84 | path = ''
85 | ): Promise {
86 | return request(config, `/repos/${owner}/${repo}/contents/${path}`);
87 | }
88 |
89 | // Add utility function for base64 encoding
90 | function bytesToBase64(bytes: Uint8Array): string {
91 | const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join('');
92 | return btoa(binString);
93 | }
94 |
95 | export async function createFile(
96 | config: GitHubConfig,
97 | owner: string,
98 | repo: string,
99 | path: string,
100 | content: string | ArrayBuffer,
101 | message = 'Add file via db3',
102 | sha?: string
103 | ): Promise {
104 | const body: any = {
105 | message,
106 | committer: createCommitter(config.userEmail)
107 | };
108 |
109 | // Handle both text and binary content
110 | if (typeof content === 'string') {
111 | body.content = btoa(content); // Handle UTF-8 text properly
112 | } else {
113 | body.content = bytesToBase64(new Uint8Array(content));
114 | }
115 |
116 | // Only include SHA if updating an existing file
117 | if (sha) {
118 | body.sha = sha;
119 | }
120 |
121 | await request(config, `/repos/${owner}/${repo}/contents/${path}`, {
122 | method: 'PUT',
123 | body: JSON.stringify(body)
124 | });
125 | }
126 |
127 | export async function deleteFile(
128 | config: GitHubConfig,
129 | owner: string,
130 | repo: string,
131 | path: string,
132 | sha: string,
133 | message = 'Delete file via db3'
134 | ): Promise {
135 | await request(config, `/repos/${owner}/${repo}/contents/${path}`, {
136 | method: 'DELETE',
137 | body: JSON.stringify({
138 | message,
139 | sha,
140 | committer: createCommitter(config.userEmail)
141 | })
142 | });
143 | }
144 |
145 | export async function deleteRepository(
146 | config: GitHubConfig,
147 | owner: string,
148 | repo: string
149 | ): Promise {
150 | await request(config, `/repos/${owner}/${repo}`, {
151 | method: 'DELETE'
152 | });
153 | }
154 |
155 | export async function checkRepo(
156 | config: GitHubConfig,
157 | owner: string,
158 | repo: string
159 | ): Promise {
160 | try {
161 | await request(config, `/repos/${owner}/${repo}`);
162 | return true;
163 | } catch (error) {
164 | return false;
165 | }
166 | }
167 |
168 | export async function deleteFolder(
169 | config: GitHubConfig,
170 | owner: string,
171 | repo: string,
172 | path: string
173 | ): Promise {
174 | const contents = await getContents(config, owner, repo, path);
175 |
176 | // Recursively delete all contents
177 | for (const item of contents) {
178 | if (item.type === 'dir') {
179 | await deleteFolder(config, owner, repo, item.path);
180 | } else {
181 | await deleteFile(config, owner, repo, item.path, item.sha);
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/src/lib/services/service.ts:
--------------------------------------------------------------------------------
1 | import type { ServiceConfig } from '$lib/types';
2 | import { DEFAULT_SERVICE_CONFIG, SERVICE_REPO_PATH, SERVICE_CONFIG_FILE } from '$lib/types';
3 | import * as github from './github';
4 |
5 | export async function initializeServiceRepo(config: github.GitHubConfig) {
6 | // First, try to find existing service repo
7 | const repos = await github.getRepositories(config);
8 | let serviceRepo = repos.find((repo) => repo.name === SERVICE_REPO_PATH);
9 |
10 | // If it doesn't exist, create it
11 | if (!serviceRepo) {
12 | serviceRepo = await github.createRepository(config, SERVICE_REPO_PATH);
13 |
14 | // Create initial config file immediately after creating repo
15 | await github.createFile(
16 | config,
17 | serviceRepo.owner.login,
18 | serviceRepo.name,
19 | SERVICE_CONFIG_FILE,
20 | JSON.stringify(
21 | {
22 | ...DEFAULT_SERVICE_CONFIG,
23 | connectedRepos: [`${serviceRepo.owner.login}/${SERVICE_REPO_PATH}`]
24 | },
25 | null,
26 | 2
27 | )
28 | );
29 |
30 | return serviceRepo;
31 | }
32 |
33 | // Try to get existing config
34 | try {
35 | const contents = await github.getContents(
36 | config,
37 | serviceRepo.owner.login,
38 | serviceRepo.name,
39 | SERVICE_CONFIG_FILE
40 | );
41 |
42 | if (!Array.isArray(contents)) {
43 | const fileContent = contents as { content: string };
44 | const configContent = atob(fileContent.content);
45 | const existingConfig = JSON.parse(configContent);
46 |
47 | // Validate and clean connected repos
48 | const validRepos = await validateAndCleanConnectedRepos(
49 | config,
50 | existingConfig.connectedRepos
51 | );
52 |
53 | // Update config if any repos were removed
54 | if (validRepos.length !== existingConfig.connectedRepos.length) {
55 | const updatedConfig = { ...existingConfig, connectedRepos: validRepos };
56 | await github.createFile(
57 | config,
58 | serviceRepo.owner.login,
59 | serviceRepo.name,
60 | SERVICE_CONFIG_FILE,
61 | JSON.stringify(updatedConfig, null, 2),
62 | 'Clean up invalid repositories',
63 | fileContent.sha
64 | );
65 | }
66 | }
67 | } catch (error) {
68 | if ((error as any).status === 404) {
69 | // Create new config file if it doesn't exist
70 | await github.createFile(
71 | config,
72 | serviceRepo.owner.login,
73 | serviceRepo.name,
74 | SERVICE_CONFIG_FILE,
75 | JSON.stringify(
76 | {
77 | ...DEFAULT_SERVICE_CONFIG,
78 | connectedRepos: [`${serviceRepo.owner.login}/${SERVICE_REPO_PATH}`]
79 | },
80 | null,
81 | 2
82 | )
83 | );
84 | } else {
85 | throw error;
86 | }
87 | }
88 |
89 | return serviceRepo;
90 | }
91 | export async function getServiceConfig(config: github.GitHubConfig): Promise {
92 | const repos = await github.getRepositories(config);
93 | const serviceRepo = repos.find((repo) => repo.name === SERVICE_REPO_PATH);
94 | if (!serviceRepo) throw new Error('Service repository not found');
95 |
96 | const contents = await github.getContents(
97 | config,
98 | serviceRepo.owner.login,
99 | serviceRepo.name,
100 | SERVICE_CONFIG_FILE
101 | );
102 |
103 | if (Array.isArray(contents)) throw new Error('Unexpected contents format');
104 |
105 | // Add a type assertion here
106 | const fileContent = contents as { content: string };
107 | const configContent = atob(fileContent.content);
108 | return JSON.parse(configContent);
109 | }
110 |
111 | export async function updateServiceConfig(
112 | config: github.GitHubConfig,
113 | serviceConfig: ServiceConfig
114 | ): Promise {
115 | const repos = await github.getRepositories(config);
116 | const serviceRepo = repos.find((repo) => repo.name === SERVICE_REPO_PATH);
117 | if (!serviceRepo) throw new Error('Service repository not found');
118 |
119 | try {
120 | // Get current file contents to get the SHA
121 | const contents = await github.getContents(
122 | config,
123 | serviceRepo.owner.login,
124 | serviceRepo.name,
125 | SERVICE_CONFIG_FILE
126 | );
127 |
128 | if (Array.isArray(contents)) throw new Error('Unexpected contents format');
129 |
130 | // Use the SHA from the existing file
131 | const fileContent = contents as { sha: string };
132 |
133 | await github.createFile(
134 | config,
135 | serviceRepo.owner.login,
136 | serviceRepo.name,
137 | SERVICE_CONFIG_FILE,
138 | JSON.stringify(serviceConfig, null, 2),
139 | 'Update service config',
140 | fileContent.sha // Pass the SHA
141 | );
142 | } catch (error) {
143 | // If file doesn't exist, create it without SHA
144 | if ((error as any).status === 404) {
145 | await github.createFile(
146 | config,
147 | serviceRepo.owner.login,
148 | serviceRepo.name,
149 | SERVICE_CONFIG_FILE,
150 | JSON.stringify(serviceConfig, null, 2),
151 | 'Create service config'
152 | );
153 | } else {
154 | throw error;
155 | }
156 | }
157 | }
158 |
159 | export async function validateAndCleanConnectedRepos(
160 | config: github.GitHubConfig,
161 | connectedRepos: string[]
162 | ): Promise {
163 | const validRepos: string[] = [];
164 |
165 | for (const repoFullName of connectedRepos) {
166 | const [owner, repo] = repoFullName.split('/');
167 | if (await github.checkRepo(config, owner, repo)) {
168 | validRepos.push(repoFullName);
169 | } else {
170 | console.log(`Repository ${repoFullName} no longer exists, removing from config`);
171 | }
172 | }
173 |
174 | return validRepos;
175 | }
176 |
--------------------------------------------------------------------------------
/src/lib/stores/reload.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 |
3 | export const needsReload = writable(false);
4 |
--------------------------------------------------------------------------------
/src/lib/stores/repositories.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 | import type { Repository } from '$lib/types';
3 |
4 | export const repositories = writable([]);
5 | export const currentRepository = writable(null);
6 |
--------------------------------------------------------------------------------
/src/lib/stores/service-config.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 | import type { ServiceConfig } from '$lib/types';
3 | import { DEFAULT_SERVICE_CONFIG } from '$lib/types';
4 |
5 | export const serviceConfig = writable(DEFAULT_SERVICE_CONFIG);
6 |
--------------------------------------------------------------------------------
/src/lib/stores/theme.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 |
3 | type Theme = 'light' | 'dark';
4 |
5 | function createThemeStore() {
6 | // Get initial theme from localStorage or system preference
7 | const getInitialTheme = (): Theme => {
8 | if (typeof window === 'undefined') return 'light';
9 |
10 | const savedTheme = localStorage.getItem('theme') as Theme;
11 | if (savedTheme) return savedTheme;
12 |
13 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
14 | };
15 |
16 | const { subscribe, set } = writable(getInitialTheme());
17 |
18 | return {
19 | subscribe,
20 | toggle: () => {
21 | if (typeof window === 'undefined') return;
22 |
23 | const newTheme = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
24 | document.documentElement.classList.remove('light', 'dark');
25 | document.documentElement.classList.add(newTheme);
26 | localStorage.setItem('theme', newTheme);
27 | set(newTheme);
28 | },
29 | // Only used for initialization
30 | set: (theme: Theme) => {
31 | if (typeof window === 'undefined') return;
32 |
33 | document.documentElement.classList.remove('light', 'dark');
34 | document.documentElement.classList.add(theme);
35 | localStorage.setItem('theme', theme);
36 | set(theme);
37 | }
38 | };
39 | }
40 |
41 | export const theme = createThemeStore();
42 |
--------------------------------------------------------------------------------
/src/lib/supabaseClient.ts:
--------------------------------------------------------------------------------
1 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
2 | import { createClient } from '@supabase/supabase-js';
3 |
4 | export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
5 |
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | export type Repository = {
2 | id: string;
3 | name: string;
4 | full_name: string;
5 | owner: {
6 | login: string;
7 | avatar_url: string;
8 | };
9 | default_branch: string;
10 | private: boolean;
11 | html_url: string;
12 | };
13 |
14 | export type FileContent = {
15 | name: string;
16 | path: string;
17 | sha: string;
18 | size: number;
19 | url: string;
20 | html_url: string;
21 | git_url: string;
22 | download_url: string;
23 | type: 'file' | 'dir';
24 | content?: string;
25 | encoding?: string;
26 | };
27 |
28 | export type ServiceConfig = {
29 | connectedRepos: string[];
30 | version: number;
31 | };
32 |
33 | export const DEFAULT_SERVICE_CONFIG: ServiceConfig = {
34 | connectedRepos: [],
35 | version: 1
36 | };
37 |
38 | export const SERVICE_REPO_PATH = '3db-service';
39 | export const SERVICE_CONFIG_FILE = 'index.json';
40 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function formatBytes(bytes: number, decimals = 1): string {
9 | if (bytes === 0) return '0 B';
10 |
11 | const k = 1024;
12 | const dm = decimals < 0 ? 0 : decimals;
13 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
14 |
15 | const i = Math.floor(Math.log(bytes) / Math.log(k));
16 |
17 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
18 | }
19 |
--------------------------------------------------------------------------------
/src/routes/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import type { LayoutServerLoad } from './$types';
2 |
3 | export const load: LayoutServerLoad = async ({ locals: { supabase, getUser } }) => {
4 | const {
5 | data: { session }
6 | } = await supabase.auth.getSession();
7 |
8 | const user = session ? await getUser() : null;
9 |
10 | return {
11 | user,
12 | session
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
167 |
168 | {#if !isLoading}
169 | {#if data.user}
170 | {#if !hasGithubApp}
171 |
174 | {:else}
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | {#if githubConfig}
183 |
184 | Repositories
185 |
186 | {#each $repositories as repo}
187 |
188 |
189 |
197 |
198 |
199 | {/each}
200 |
201 |
202 | {:else}
203 | Loading repositories...
204 | {/if}
205 |
206 |
207 |
208 | {#if $needsReload}
209 |
210 |
211 |
Reload to see changes. You may need to wait a few seconds.
212 |
213 | {/if}
214 | (uploadDialogOpen = true)}
217 | disabled={!$currentRepository}
218 | >
219 |
220 | Upload File
221 |
222 | (createRepoDialogOpen = true)}>
223 |
224 | Link or Create Repo
225 |
226 |
227 |
228 |
229 |
230 | {@render children()}
231 |
232 | {#if githubConfig}
233 |
238 |
239 |
246 | {/if}
247 |
248 |
249 | {/if}
250 | {:else}
251 | {@render children()}
252 | {/if}
253 | {/if}
254 |
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
2 | import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit';
3 | import type { LoadEvent } from '@sveltejs/kit';
4 |
5 | export const load = async ({ fetch, depends }: LoadEvent) => {
6 | depends('supabase:auth');
7 |
8 | const supabase = createSupabaseLoadClient({
9 | supabaseUrl: PUBLIC_SUPABASE_URL,
10 | supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
11 | event: { fetch },
12 | serverSession: null
13 | });
14 |
15 | // First check session
16 | const {
17 | data: { session }
18 | } = await supabase.auth.getSession();
19 |
20 | let user = null;
21 | if (session) {
22 | const {
23 | data: { user: userData },
24 | error
25 | } = await supabase.auth.getUser();
26 | if (error) {
27 | console.error('Error fetching user:', error);
28 | } else {
29 | user = userData;
30 | }
31 | }
32 |
33 | return {
34 | supabase,
35 | session,
36 | user
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
55 |
56 |
57 | {#if !isLoading}
58 | {#if !data.user}
59 |
60 |
3db
61 |
62 |
63 | Login with GitHub
64 |
65 |
66 |
67 |
68 |
Use GitHub as a CDN
69 |
70 | 3db provides a simple interface to use GitHub repositories as a content delivery
71 | network or file store. Pretty flaky, but works.
72 |
73 |
74 |
75 |
76 |
Features:
77 |
78 | Create and connect multiple repositories
79 | Upload and delete files
80 | Browse files and folders
81 | Get direct CDN links
82 |
83 |
84 |
85 |
86 |
How it works:
87 |
88 | A repository called '3db-service' will be created to store metadata. You can then
89 | create or connect repositories to use as storage. Files are stored in public GitHub
90 | repositories and served via GitHub's CDN.
91 |
92 |
93 |
94 |
95 |
Warning
96 |
97 | This is an experimental project. Only use for non-critical data. Files are public and
98 | GitHub's 5GB repository limit applies.
99 |
100 |
101 |
102 |
111 |
112 |
113 | {:else if !githubConfig}
114 |
115 |
116 | Unable to access GitHub. Please try logging in again.
117 |
118 |
119 | {:else}
120 |
121 |
122 |
123 | {/if}
124 | {/if}
125 |
126 |
--------------------------------------------------------------------------------
/src/routes/api/github/check-installation/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import type { RequestHandler } from './$types';
3 |
4 | export const GET: RequestHandler = async ({ locals: { supabase, getUser } }) => {
5 | const user = await getUser();
6 |
7 | if (!user) {
8 | return json({ installed: false });
9 | }
10 |
11 | try {
12 | const {
13 | data: { session }
14 | } = await supabase.auth.getSession();
15 |
16 | if (!session?.provider_token) {
17 | return json({ installed: false });
18 | }
19 |
20 | // Make a request to GitHub API to check if the app is installed
21 | const response = await fetch('https://api.github.com/user/installations', {
22 | headers: {
23 | Authorization: `Bearer ${session.provider_token}`,
24 | Accept: 'application/vnd.github.v3+json'
25 | }
26 | });
27 |
28 | const data = await response.json();
29 | const installed = data.total_count > 0;
30 |
31 | return json({ installed });
32 | } catch (error) {
33 | console.error('Error checking GitHub app installation:', error);
34 | return json({ installed: false });
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/src/routes/auth/auth-code-error/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
Authentication Error
7 |
There was an error during authentication.
8 |
goto('/')}>Return to Home
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/auth/callback/+server.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from '@sveltejs/kit';
2 | import type { RequestHandler } from './$types';
3 |
4 | export const GET: RequestHandler = async ({ url, locals: { supabase } }) => {
5 | const code = url.searchParams.get('code');
6 | const next = url.searchParams.get('next') ?? '/';
7 |
8 | if (code) {
9 | const { error } = await supabase.auth.exchangeCodeForSession(code);
10 | if (error) {
11 | console.error('Auth error:', error);
12 | throw redirect(303, '/');
13 | }
14 | }
15 |
16 | throw redirect(303, next);
17 | };
18 |
--------------------------------------------------------------------------------
/static/BasierSquareMono.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tnixc/3db/c8683196ee815be5215f3ba109a205091c5fc9c0/static/BasierSquareMono.woff2
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tnixc/3db/c8683196ee815be5215f3ba109a205091c5fc9c0/static/favicon.png
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | // import adapter from '@sveltejs/adapter-auto';
2 | import adapter from '@sveltejs/adapter-vercel';
3 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
4 |
5 | /** @type {import('@sveltejs/kit').Config} */
6 | const config = {
7 | // Consult https://svelte.dev/docs/kit/integrations
8 | // for more information about preprocessors
9 | preprocess: vitePreprocess(),
10 |
11 | kit: {
12 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
13 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
14 | // See https://svelte.dev/docs/kit/adapters for more information about adapters.
15 | adapter: adapter()
16 | }
17 | };
18 |
19 | export default config;
20 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { fontFamily } from 'tailwindcss/defaultTheme';
2 | import type { Config } from 'tailwindcss';
3 | import tailwindcssAnimate from 'tailwindcss-animate';
4 |
5 | const config: Config = {
6 | darkMode: ['class'],
7 | content: ['./src/**/*.{html,js,svelte,ts}'],
8 | safelist: ['dark'],
9 | theme: {
10 | container: {
11 | center: true,
12 | padding: '2rem',
13 | screens: {
14 | '2xl': '1400px'
15 | }
16 | },
17 | extend: {
18 | colors: {
19 | border: 'hsl(var(--border) / )',
20 | input: 'hsl(var(--input) / )',
21 | ring: 'hsl(var(--ring) / )',
22 | background: 'hsl(var(--background) / )',
23 | foreground: 'hsl(var(--foreground) / )',
24 | primary: {
25 | DEFAULT: 'hsl(var(--primary) / )',
26 | foreground: 'hsl(var(--primary-foreground) / )'
27 | },
28 | secondary: {
29 | DEFAULT: 'hsl(var(--secondary) / )',
30 | foreground: 'hsl(var(--secondary-foreground) / )'
31 | },
32 | destructive: {
33 | DEFAULT: 'hsl(var(--destructive) / )',
34 | foreground: 'hsl(var(--destructive-foreground) / )'
35 | },
36 | muted: {
37 | DEFAULT: 'hsl(var(--muted) / )',
38 | foreground: 'hsl(var(--muted-foreground) / )'
39 | },
40 | accent: {
41 | DEFAULT: 'hsl(var(--accent) / )',
42 | foreground: 'hsl(var(--accent-foreground) / )'
43 | },
44 | popover: {
45 | DEFAULT: 'hsl(var(--popover) / )',
46 | foreground: 'hsl(var(--popover-foreground) / )'
47 | },
48 | card: {
49 | DEFAULT: 'hsl(var(--card) / )',
50 | foreground: 'hsl(var(--card-foreground) / )'
51 | },
52 | sidebar: {
53 | DEFAULT: 'hsl(var(--sidebar-background))',
54 | foreground: 'hsl(var(--sidebar-foreground))',
55 | primary: 'hsl(var(--sidebar-primary))',
56 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
57 | accent: 'hsl(var(--sidebar-accent))',
58 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
59 | border: 'hsl(var(--sidebar-border))',
60 | ring: 'hsl(var(--sidebar-ring))'
61 | }
62 | },
63 | borderRadius: {
64 | xl: 'calc(var(--radius) + 4px)',
65 | lg: 'var(--radius)',
66 | md: 'calc(var(--radius) - 2px)',
67 | sm: 'calc(var(--radius) - 4px)'
68 | },
69 | fontFamily: {
70 | sans: [...fontFamily.sans]
71 | },
72 | keyframes: {
73 | 'accordion-down': {
74 | from: { height: '0' },
75 | to: { height: 'var(--bits-accordion-content-height)' }
76 | },
77 | 'accordion-up': {
78 | from: { height: 'var(--bits-accordion-content-height)' },
79 | to: { height: '0' }
80 | },
81 | 'caret-blink': {
82 | '0%,70%,100%': { opacity: '1' },
83 | '20%,50%': { opacity: '0' }
84 | }
85 | },
86 | animation: {
87 | 'accordion-down': 'accordion-down 0.2s ease-out',
88 | 'accordion-up': 'accordion-up 0.2s ease-out',
89 | 'caret-blink': 'caret-blink 1.25s ease-out infinite'
90 | }
91 | }
92 | },
93 | plugins: [tailwindcssAnimate]
94 | };
95 |
96 | export default config;
97 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | // "checkJs": true,
5 | "allowJs": false,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler"
13 | }
14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
16 | //
17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18 | // from the referenced tsconfig.json - TypeScript does not merge them in
19 | }
20 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()]
6 | });
7 |
--------------------------------------------------------------------------------