Initial commit
This commit is contained in:
commit
9c24e7943a
|
@ -0,0 +1,14 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
quote_type = single
|
||||
|
||||
[*.{yml, md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,8 @@
|
|||
# project endpoint (required) [example: http://localhost/v1]
|
||||
VITE_APPWRITE_ENDPOINT=http://localhost/v1
|
||||
|
||||
# project id (required) [example: 638871b363904655d784]
|
||||
VITE_APPWRITE_PROJECT_ID=638871b363904655d784
|
||||
|
||||
# project hostname (required) [example: http://localhost:5173]
|
||||
VITE_HOSTNAME=http://localhost:5173
|
|
@ -0,0 +1,27 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# appwrite
|
||||
appwrite
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"proseWrap": "preserve",
|
||||
"printWidth": 180,
|
||||
"useTabs": true,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Ludvík Prokopec
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,229 @@
|
|||
# Svelte + Appwrite = 🚀
|
||||
|
||||
## Appwrite svelte template
|
||||
|
||||
Blazing fast development with done backend and fully-prepared frontend.
|
||||
|
||||
CMS ready!
|
||||
|
||||
## Appwrite installation
|
||||
|
||||
[Appwrite installation](https://appwrite.io/docs/installation)
|
||||
|
||||
## Frontend included
|
||||
|
||||
* tailwind
|
||||
* scss
|
||||
* css reset
|
||||
* typescript
|
||||
* routing
|
||||
* ready routes
|
||||
* oauth
|
||||
* files upload, download
|
||||
* folder structure
|
||||
* common components
|
||||
* service worker
|
||||
* path aliases
|
||||
* database realtime subscribers
|
||||
* database paginate, infinity scroll
|
||||
* i18n
|
||||
* cms
|
||||
* cms forms components
|
||||
* vite
|
||||
* prettier
|
||||
* editorconfig
|
||||
* icons: [Bootstrap icons](https://icons.getbootstrap.com/)
|
||||
|
||||
## Database subscribers
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Collection } from '$lib/database'
|
||||
import { Query } from 'appwrite'
|
||||
|
||||
const collection = new Collection('[database-id]', '[collection-id]')
|
||||
const [subscriber, loading] = collection.createSubscriber([Query.limit(5) /*, ...queries */])
|
||||
// listen changes (update, delete) in database and automatically rerender on change
|
||||
// current data = [{ name: 'John', lastName: 'Doe' }, ...]
|
||||
|
||||
const insertSubscriber = collection.createObserver()
|
||||
// listen changes (create) in database and automatically rerender on change
|
||||
|
||||
const [paginator, paginatorInitalLoading] = collection.createPaginate(10, [/* ...queries */])
|
||||
// paginate the collection of documents with limit and automatically rerender on change
|
||||
// paginator.next() makes the next request for items, paginator store automatically rerender on next load
|
||||
|
||||
const [scrollData, scrollDispatch] = collection.createInfinityScrollDispatcher(10, [/* ...queries */], { /* intersection observer options */ })
|
||||
// load next data after scroll to anchor (scrollDispatch) element
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if $loading}
|
||||
<p>Loading data from database...</p>
|
||||
{:else}
|
||||
{#each [...$subscriber, ...$insertSubscriber] as item}
|
||||
<p>{item.name}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- scroll dispatcher example -->
|
||||
<div>
|
||||
{#each $scrollData as item}
|
||||
<p>{item.name}</p>
|
||||
{/each}
|
||||
<div use:scrollDispatch on:fetch={(e) => console.log(e) /* on every fetch from scroll dispatcher do some action */} />
|
||||
</div>
|
||||
```
|
||||
|
||||
## Files subscribers
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Bucket } from '$lib/storage'
|
||||
import { Query } from 'appwrite'
|
||||
|
||||
const bucket = new Bucket('[bucket-id]')
|
||||
const [files, loading] = bucket.createSubscriber([Query.limit(5) /*, ...queries */])
|
||||
// listen changes (update, delete) in files and automatically rerender on change
|
||||
|
||||
const insertSubscriber = bucket.createObserver()
|
||||
// listen changes (create) in files and automatically rerender on change
|
||||
|
||||
const [upload, dispatch] = storage.createUploadDispatcher(/* many files ? true : false, default = false */)
|
||||
|
||||
const [content, loading] = storage.getFileContent('6391f7c70ede82115575')
|
||||
// get file content and automatically rerender on file update
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<input type="file" use:upload />
|
||||
<button on:click={() => dispatch().then(uploadedFile => console.log(uploadedFile))}>Upload</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
Routes can be added in `__routes.svelte` file. Every route is fetched lazyly.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import Router from '$lib/router/Router.svelte'
|
||||
|
||||
import Layout from '$src/__layout.svelte'
|
||||
import Loading from '$src/__loading.svelte'
|
||||
import Error from '$src/__error.svelte'
|
||||
</script>
|
||||
|
||||
<Router
|
||||
layout={Layout}
|
||||
loading={Loading}
|
||||
error={Error}
|
||||
routes={[
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('$routes/index.svelte'),
|
||||
},
|
||||
{
|
||||
path: '/oauth',
|
||||
component: () => import('$routes/oauth/index.svelte'),
|
||||
},
|
||||
{
|
||||
path: '/oauth/failure',
|
||||
component: () => import('$routes/oauth/failure.svelte'),
|
||||
},
|
||||
{
|
||||
path: '/oauth/success',
|
||||
component: () => import('$routes/oauth/success.svelte'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Routes structure
|
||||
|
||||
`__layout.svelte` the default layout for every page
|
||||
|
||||
`__error.svelte` the error page (404 error)
|
||||
|
||||
`__loading.svelte` the default loading component
|
||||
|
||||
`__routes.svelte` the file includes all routes in application
|
||||
|
||||
## Social auth
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { account, url } from '$lib/stores/appwrite'
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<button on:click={() => account.createOAuth2Session('github', url.oauth.success, url.oauth.failure)}>
|
||||
Github
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## i18n
|
||||
|
||||
Locale file `src/locales/en.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"page": {
|
||||
"home": {
|
||||
"title": "Appwrite svelte rocket start 🚀"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { _, locale, locales } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>{$_('page.home.title')}</h1>
|
||||
|
||||
<div>
|
||||
<p>Change language:</p>
|
||||
|
||||
<select bind:value={$locale}>
|
||||
{#each $locales as locale}
|
||||
<option value={locale}>{locale}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## path aliases
|
||||
|
||||
`$lib` = `src/lib`
|
||||
|
||||
`$root` = `/`
|
||||
|
||||
`$src` = `src`
|
||||
|
||||
`$cms` = `src/cms`
|
||||
|
||||
`$routes` = `src/routes`
|
||||
|
||||
## commands
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run appwrite
|
||||
```
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Svelte + Appwrite</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
/**
|
||||
* svelte-preprocess cannot figure out whether you have
|
||||
* a value or a type, so tell TypeScript to enforce using
|
||||
* `import type` instead of `import` for Types.
|
||||
*/
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* To have warnings / errors of the Svelte compiler at the
|
||||
* correct position, enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable this if you'd like to use dynamic types.
|
||||
*/
|
||||
"checkJs": true
|
||||
},
|
||||
/**
|
||||
* Use global.d.ts instead of compilerOptions.types
|
||||
* to avoid limiting type declarations.
|
||||
*/
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "appwrite-svelte-rocket-start",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"appwrite": "docker compose -f ./appwrite/docker-compose.yml up"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.1.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"flowbite-svelte": "^0.28.4",
|
||||
"postcss": "^8.4.19",
|
||||
"sass": "^1.56.1",
|
||||
"svelte": "^3.52.0",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bytemd/plugin-gfm": "^1.17.4",
|
||||
"appwrite": "^10.1.0",
|
||||
"bytemd": "^1.17.4",
|
||||
"svelte-i18n": "^3.6.0",
|
||||
"svelte-routing": "^1.6.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
server {
|
||||
server_name cms.example.com;
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:xxxx;
|
||||
}
|
||||
}
|
||||
server {
|
||||
server_name *.example.com;
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:xxxx;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
self.addEventListener("install", (e) => { })
|
||||
|
||||
self.addEventListener('fetch', (e) => { })
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import './main.scss'
|
||||
import { i18n, isLoading } from './locales/i18n'
|
||||
import { onMount } from 'svelte'
|
||||
import { Router } from '$lib/router'
|
||||
|
||||
import Routes from './__routes.svelte'
|
||||
|
||||
let isMounted = false
|
||||
onMount(() => {
|
||||
/** init i18n */
|
||||
i18n()
|
||||
|
||||
/** register service worker */
|
||||
if ('serviceWorker' in window.navigator) {
|
||||
window.navigator.serviceWorker.register('/serviceworker.js', {
|
||||
scope: '/',
|
||||
})
|
||||
}
|
||||
|
||||
isMounted = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<Router>
|
||||
{#if !$isLoading && isMounted}
|
||||
<Routes />
|
||||
{/if}
|
||||
</Router>
|
|
@ -0,0 +1 @@
|
|||
<h1>Error 404</h1>
|
|
@ -0,0 +1,3 @@
|
|||
<main class="container">
|
||||
<slot />
|
||||
</main>
|
|
@ -0,0 +1 @@
|
|||
<p>Loading...</p>
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import Router from '$lib/router/Router.svelte'
|
||||
|
||||
import Layout from '$src/__layout.svelte'
|
||||
import Loading from '$src/__loading.svelte'
|
||||
import Error from '$src/__error.svelte'
|
||||
</script>
|
||||
|
||||
<Router
|
||||
layout={Layout}
|
||||
loading={Loading}
|
||||
error={Error}
|
||||
routes={[
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('$routes/index.svelte'),
|
||||
},
|
||||
{
|
||||
path: '/oauth',
|
||||
component: () => import('$routes/oauth/index.svelte'),
|
||||
},
|
||||
{
|
||||
path: '/oauth/failure',
|
||||
component: () => import('$routes/oauth/failure.svelte'),
|
||||
},
|
||||
{
|
||||
path: '/oauth/success',
|
||||
component: () => import('$routes/oauth/success.svelte'),
|
||||
},
|
||||
]}
|
||||
/>
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -0,0 +1,10 @@
|
|||
import FileDrop from './components/FileDrop.svelte'
|
||||
import Input from './components/Input.svelte'
|
||||
import Sortable from './components/Sortable.svelte'
|
||||
import BlockQuote from './components/BlockQuote.svelte'
|
||||
import MarkdownEditor from './components/MarkdownEditor.svelte'
|
||||
import MarkdownRenderer from './components/MarkdownRenderer.svelte'
|
||||
|
||||
import { Radio, Checkbox, Fileupload as FileUpload, Range, Select, Textarea, Toggle, Hr as HorizontalRule, Button, ButtonGroup } from 'flowbite-svelte'
|
||||
|
||||
export { FileDrop, Input, Radio, Checkbox, FileUpload, Range, Select, Textarea, Toggle, Sortable, BlockQuote, HorizontalRule, MarkdownEditor, MarkdownRenderer, Button, ButtonGroup }
|
|
@ -0,0 +1,9 @@
|
|||
<script>
|
||||
import { Blockquote, P } from 'flowbite-svelte'
|
||||
</script>
|
||||
|
||||
<Blockquote border bg class="p-4 my-4">
|
||||
<P height="relaxed">
|
||||
<slot />
|
||||
</P>
|
||||
</Blockquote>
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { Dropzone } from 'flowbite-svelte'
|
||||
|
||||
export let rules = []
|
||||
</script>
|
||||
|
||||
<Dropzone on:blur on:change on:click on:focus on:mouseenter on:mouseleave on:mouseover>
|
||||
<svg aria-hidden="true" class="mb-3 w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{rules.join(', ')}</p>
|
||||
</Dropzone>
|
|
@ -0,0 +1,45 @@
|
|||
<script lang="ts">
|
||||
import { Input, Label, Helper } from 'flowbite-svelte'
|
||||
import id from './idGenerator'
|
||||
|
||||
const elementId = id('input-')
|
||||
|
||||
export let label = ''
|
||||
export let value = ''
|
||||
export let error: string = null
|
||||
|
||||
export let type:
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
| 'email'
|
||||
| 'file'
|
||||
| 'hidden'
|
||||
| 'image'
|
||||
| 'month'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'reset'
|
||||
| 'submit'
|
||||
| 'tel'
|
||||
| 'text'
|
||||
| 'time'
|
||||
| 'url'
|
||||
| 'week'
|
||||
| 'search' = 'text'
|
||||
</script>
|
||||
|
||||
<div class="mb-6">
|
||||
<Label for={elementId} color={error ? 'red' : null} class="mb-2">{label}</Label>
|
||||
|
||||
<Input bind:value {type} color={error ? 'red' : null} id={elementId} {...$$props}>
|
||||
<slot />
|
||||
</Input>
|
||||
|
||||
{#if error}
|
||||
<Helper class="mt-2" color="red">
|
||||
<span class="font-medium">Error!</span>
|
||||
{error}
|
||||
</Helper>
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
<div class="h-full w-full top-0 left-0 flex justify-center items-center">
|
||||
<div class="animate-spin inline-block w-6 h-6 border-[3px] border-current border-t-transparent text-current rounded-full" role="status" aria-label="loading">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import 'bytemd/dist/index.css'
|
||||
import { Editor } from 'bytemd'
|
||||
import gfm from '@bytemd/plugin-gfm'
|
||||
|
||||
const plugins = [gfm()]
|
||||
|
||||
export let value = ''
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<template>
|
||||
<Editor {value} {plugins} on:change={(e) => (value = e.detail.value)} />
|
||||
</template>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import 'bytemd/dist/index.css'
|
||||
import { Viewer } from 'bytemd'
|
||||
import gfm from '@bytemd/plugin-gfm'
|
||||
|
||||
const plugins = [gfm()]
|
||||
|
||||
export let value = ''
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<template>
|
||||
<Viewer {value} {plugins} />
|
||||
</template>
|
||||
</div>
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
|
||||
export let list
|
||||
export let key = 'id'
|
||||
export let element = 'div'
|
||||
export let active = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let source = null
|
||||
let isOver = false
|
||||
|
||||
const start = (e, id) => {
|
||||
e.dataTransfer.clearData()
|
||||
e.dataTransfer.setData('text/plain', id)
|
||||
}
|
||||
|
||||
const over = (target) => isOver !== target.id && (isOver = target.id)
|
||||
const leave = () => isOver === source?.id && (isOver = false)
|
||||
|
||||
const reorder = (source, target) => {
|
||||
if (!active || !source || !target) return
|
||||
if (source.id === target.id) return
|
||||
|
||||
const { order: sourceIndex } = source
|
||||
const { order: targetIndex } = target
|
||||
|
||||
isOver = false
|
||||
|
||||
list[sourceIndex] = [list[targetIndex], (list[targetIndex] = list[sourceIndex])][0]
|
||||
list = list
|
||||
|
||||
dispatch('reorder', { source, target })
|
||||
source = null
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each list as item, order (item[key])}
|
||||
<svelte:element
|
||||
this={element}
|
||||
draggable={active}
|
||||
on:dragstart={(e) => (source = { order, id: item[key] }) && start(e, source.id)}
|
||||
on:dragover|preventDefault={() => over({ order, id: item[key] })}
|
||||
on:dragleave={leave}
|
||||
on:dragenter|preventDefault={() => null}
|
||||
on:drop|preventDefault={() => reorder(source, { order, id: item[key] })}
|
||||
animate:flip={{ duration: source !== null ? 300 : 0 }}
|
||||
class:over={item[key] === isOver}
|
||||
>
|
||||
<slot {item} {order} index={order} />
|
||||
</svelte:element>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.over {
|
||||
border-color: rgba(48, 12, 200, 0.2);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,6 @@
|
|||
const gen = (function* () {
|
||||
let index = -1
|
||||
while (true) yield index++
|
||||
})()
|
||||
|
||||
export default (pre: string = '') => `${pre}${gen.next().value}`
|
|
@ -0,0 +1,22 @@
|
|||
import { Client, Account, Databases, Storage, Teams, Functions, Locale, Avatars } from 'appwrite'
|
||||
|
||||
const client = new Client()
|
||||
const account = new Account(client)
|
||||
const databases = new Databases(client)
|
||||
const storage = new Storage(client)
|
||||
const teams = new Teams(client)
|
||||
const functions = new Functions(client)
|
||||
const locale = new Locale(client)
|
||||
const avatars = new Avatars(client)
|
||||
|
||||
const url = {
|
||||
oauth: {
|
||||
success: `${import.meta.env.VITE_HOSTNAME}/oauth/success`,
|
||||
failure: `${import.meta.env.VITE_HOSTNAME}/oauth/failure`
|
||||
}
|
||||
}
|
||||
|
||||
client.setEndpoint(import.meta.env.VITE_APPWRITE_ENDPOINT).setProject(import.meta.env.VITE_APPWRITE_PROJECT_ID)
|
||||
|
||||
export default client
|
||||
export { client, account, url, databases, storage, teams, functions, locale, avatars }
|
|
@ -0,0 +1,31 @@
|
|||
import type { Models, RealtimeResponseEvent } from 'appwrite'
|
||||
import { writable } from 'svelte/store'
|
||||
import { account, client } from './appwrite'
|
||||
|
||||
const userStore = writable<Models.Account<Models.Preferences>>(null)
|
||||
const loadingStore = writable(true)
|
||||
|
||||
client.subscribe('account', (response: RealtimeResponseEvent<any>) => {
|
||||
if (response.events.includes('users.*.sessions.*.delete')) {
|
||||
return userStore.set(null)
|
||||
}
|
||||
|
||||
if (response.events.includes('users.*.sessions.*.update')) {
|
||||
return userStore.set(response.payload)
|
||||
}
|
||||
})
|
||||
|
||||
account.get().then(data => {
|
||||
userStore.set(data)
|
||||
loadingStore.set(false)
|
||||
})
|
||||
|
||||
const user = {
|
||||
subscribe: userStore.subscribe,
|
||||
logout: () => account.deleteSession('current'),
|
||||
account: account
|
||||
}
|
||||
|
||||
const isLoading = { subscribe: loadingStore.subscribe }
|
||||
|
||||
export { account, user, isLoading }
|
|
@ -0,0 +1,15 @@
|
|||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
|
||||
box-shadow: #d1d5db 0px -4px 0px inset, rgba(0, 0, 0, 0.4) 0px 1px 1px;
|
||||
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { link } from '$lib/router'
|
||||
|
||||
export let href: string | null = null
|
||||
|
||||
let className = ''
|
||||
export { className as class }
|
||||
|
||||
const isValidHttpUrl = (string) => {
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(string)
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
{#if isValidHttpUrl(href)}
|
||||
<a {href} class={className}>
|
||||
<slot />
|
||||
</a>
|
||||
{:else}
|
||||
<a {href} class={className} use:link>
|
||||
<slot />
|
||||
</a>
|
||||
{/if}
|
||||
{:else}
|
||||
<button class={className} on:click>
|
||||
<slot />
|
||||
</button>
|
||||
{/if}
|
|
@ -0,0 +1,5 @@
|
|||
<div class="h-full w-full top-0 left-0 flex justify-center items-center">
|
||||
<div class="animate-spin inline-block w-6 h-6 border-[3px] border-current border-t-transparent text-current rounded-full" role="status" aria-label="loading">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
export let columns = 3
|
||||
export let gap = 4
|
||||
</script>
|
||||
|
||||
<div class="masonry-grid columns-{columns} gap-x-{gap}">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(div.masonry-grid *) {
|
||||
break-inside: avoid;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
<div class="flex">
|
||||
<aside class="sidebar__sidebar w-[30%]">
|
||||
<slot name="aside" />
|
||||
</aside>
|
||||
|
||||
<main class="overflow-auto flex-1">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
export let space = '3rem'
|
||||
</script>
|
||||
|
||||
<div class="layout-stack" style="--space: {space};">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.layout-stack) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
& > * {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
& > * + * {
|
||||
margin-block-start: var(--space, 1.5rem);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,178 @@
|
|||
import { writable } from 'svelte/store'
|
||||
import { databases, client } from './appwrite'
|
||||
import { Models, Query, RealtimeResponseEvent } from 'appwrite'
|
||||
import { ID } from 'appwrite'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
class Collection {
|
||||
constructor(protected databaseId: string, protected collectionId: string) { }
|
||||
|
||||
createDocument(data: { [key: string]: any } = {}, permissions: string[] = null) {
|
||||
return databases.createDocument(this.databaseId, this.collectionId, ID.unique(), data, permissions)
|
||||
}
|
||||
|
||||
updateDocument(documentId: string, data: { [key: string]: any } = {}, permissions: string[] = null) {
|
||||
if (permissions.length === 0 && Object.keys(data).length === 0) return
|
||||
|
||||
return databases.updateDocument(this.databaseId, this.collectionId, documentId, data, permissions)
|
||||
}
|
||||
|
||||
deleteDocument(documentId: string) {
|
||||
return databases.deleteDocument(this.databaseId, this.collectionId, documentId)
|
||||
}
|
||||
|
||||
createObserver() {
|
||||
const dataStore = writable<Models.Document[]>([])
|
||||
|
||||
client.subscribe(`databases.${this.databaseId}.collections.${this.collectionId}.documents`, (response: RealtimeResponseEvent<any>) => {
|
||||
if (response.events.includes(`databases.${this.databaseId}.collections.${this.collectionId}.documents.*.create`)) {
|
||||
dataStore.update(current => {
|
||||
current.push(response.payload)
|
||||
return current
|
||||
})
|
||||
|
||||
this.subscribeCollectionUpdate(response.payload, dataStore)
|
||||
}
|
||||
})
|
||||
|
||||
return { subscribe: dataStore.subscribe }
|
||||
}
|
||||
|
||||
createSubscriber(queries: string[] = []) {
|
||||
const loadingStore = writable(true)
|
||||
const dataStore = writable<Models.Document[]>([])
|
||||
|
||||
databases.listDocuments(this.databaseId, this.collectionId, queries).then(data => {
|
||||
data.documents.forEach((document) => this.subscribeCollectionUpdate(document, dataStore))
|
||||
|
||||
dataStore.set(data.documents)
|
||||
loadingStore.set(false)
|
||||
})
|
||||
|
||||
return [{ subscribe: dataStore.subscribe }, { subscribe: loadingStore.subscribe }] as const
|
||||
}
|
||||
|
||||
createPaginate(limit: number, queries: string[] = []) {
|
||||
const dataStore = writable<Models.Document[]>([])
|
||||
const loadingStore = writable(true)
|
||||
let offset = 0
|
||||
|
||||
const store = {
|
||||
subscribe: dataStore.subscribe,
|
||||
async next() {
|
||||
const data = await databases.listDocuments(this.databaseId, this.collectionId, [...queries, Query.limit(limit), Query.offset(offset)])
|
||||
data.documents.forEach((document) => this.subscribeCollectionUpdate(document, dataStore))
|
||||
|
||||
dataStore.update(current => [...current, ...data.documents])
|
||||
offset += limit
|
||||
}
|
||||
}
|
||||
|
||||
store.next().then(() => loadingStore.set(false))
|
||||
|
||||
return [store, { subscribe: loadingStore.subscribe }] as const
|
||||
}
|
||||
|
||||
createInfinityScrollDispatcher(limit: number, queries: string[] = [], observerOptions: IntersectionObserverInit = {}) {
|
||||
const dataStore = writable<Models.Document[]>([])
|
||||
let lastId: string = null
|
||||
|
||||
databases.listDocuments(this.databaseId, this.collectionId, [...queries, Query.limit(limit)]).then(firstData => {
|
||||
dataStore.set(firstData.documents)
|
||||
firstData.documents.forEach((document) => this.subscribeCollectionUpdate(document, dataStore))
|
||||
lastId = firstData.documents[firstData.documents.length - 1].$id
|
||||
})
|
||||
|
||||
const observer = new IntersectionObserver((entries, me) => {
|
||||
if (lastId === null) return
|
||||
|
||||
entries.forEach(entry => {
|
||||
if (!entry.isIntersecting) return
|
||||
|
||||
databases.listDocuments(this.databaseId, this.collectionId, [...queries, Query.limit(limit), Query.cursorAfter(lastId)]).then((data) => {
|
||||
dataStore.update(current => {
|
||||
current.push(...data.documents)
|
||||
lastId = current[current.length - 1].$id
|
||||
return current
|
||||
})
|
||||
|
||||
data.documents.forEach((document) => this.subscribeCollectionUpdate(document, dataStore))
|
||||
|
||||
entry.target.dispatchEvent(new CustomEvent('fetch', entry.target as CustomEventInit<HTMLElement>))
|
||||
})
|
||||
})
|
||||
}, observerOptions)
|
||||
|
||||
const directive = (node: HTMLElement) => {
|
||||
observer.observe(node)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
observer.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [{ subscribe: dataStore.subscribe }, directive] as const
|
||||
}
|
||||
|
||||
protected subscribeCollectionUpdate(document: Models.Document, store: Writable<Models.Document[]>) {
|
||||
client.subscribe(`databases.${this.databaseId}.collections.${this.collectionId}.documents.${document.$id}`, (response: RealtimeResponseEvent<any>) => {
|
||||
if (response.events.includes(`databases.${this.databaseId}.collections.${this.collectionId}.documents.${document.$id}.delete`)) {
|
||||
store.update(current => {
|
||||
current.splice(current.indexOf(document), 1)
|
||||
return current
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (response.events.includes(`databases.${this.databaseId}.collections.${this.collectionId}.documents.${document.$id}.update`)) {
|
||||
store.update(current => {
|
||||
current[current.indexOf(document)] = response.payload
|
||||
return current
|
||||
})
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Document {
|
||||
constructor(protected databaseId: string, protected collectionId: string, protected documentId: string) { }
|
||||
|
||||
createSubscriber() {
|
||||
const dataStore = writable<Models.Document>(null)
|
||||
const loadingStore = writable(true)
|
||||
|
||||
databases.getDocument(this.databaseId, this.collectionId, this.documentId).then(data => {
|
||||
dataStore.set(data)
|
||||
loadingStore.set(false)
|
||||
})
|
||||
|
||||
client.subscribe(`databases.${this.databaseId}.collections.${this.collectionId}.documents.${this.documentId}`, (response: RealtimeResponseEvent<any>) => {
|
||||
if (response.events.includes(`databases.${this.databaseId}.collections.${this.collectionId}.documents.${this.documentId}.update`)) {
|
||||
dataStore.set(response.payload)
|
||||
return
|
||||
}
|
||||
|
||||
if (response.events.includes(`databases.${this.databaseId}.collections.${this.collectionId}.documents.${this.documentId}.delete`)) {
|
||||
dataStore.set(null)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return [{ subscribe: dataStore.subscribe }, { subscribe: loadingStore.subscribe }] as const
|
||||
}
|
||||
|
||||
delete() {
|
||||
return databases.deleteDocument(this.databaseId, this.collectionId, this.documentId)
|
||||
}
|
||||
|
||||
update(data: { [key: string]: any } = {}, permissions: string[] = []) {
|
||||
if (permissions.length === 0 && Object.keys(data).length === 0) return
|
||||
|
||||
return databases.updateDocument(this.databaseId, this.collectionId, this.documentId, data, permissions)
|
||||
}
|
||||
}
|
||||
|
||||
export { Collection, Document }
|
|
@ -0,0 +1,8 @@
|
|||
import { Route, Router, link, links, Link } from 'svelte-routing'
|
||||
import { navigate, back, forward } from './router/navigate'
|
||||
import Redirect from "./router/Redirect.svelte"
|
||||
import ProtectedRoute from "./router/ProtectedRoute.svelte"
|
||||
import LazyRoute from "./router/LazyRoute.svelte"
|
||||
import defineRoutes from "./router/routes"
|
||||
|
||||
export { Route, Router, Link, link, links, navigate, back, forward, Redirect, ProtectedRoute, LazyRoute, defineRoutes }
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import type { ComponentType, SvelteComponentTyped } from 'svelte'
|
||||
|
||||
export let path: string
|
||||
export let component: () => Promise<any>
|
||||
export let loading: ComponentType<SvelteComponentTyped<any>> | null = null
|
||||
|
||||
import { Route } from '$lib/router'
|
||||
import LazyRouteGuard from './LazyRouteGuard.svelte'
|
||||
</script>
|
||||
|
||||
<Route {path} let:location let:params>
|
||||
<LazyRouteGuard {location} {params} {component} {loading} />
|
||||
</Route>
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { onMount, SvelteComponent } from 'svelte'
|
||||
import type { ComponentType, SvelteComponentTyped } from 'svelte'
|
||||
import type { RouteLocation, RouteParams } from 'svelte-routing/types/Route'
|
||||
|
||||
export let component: (() => Promise<any>) | ComponentType<SvelteComponentTyped<any>>
|
||||
export let loading: ComponentType<SvelteComponentTyped<any>> | null = null
|
||||
export let location: RouteLocation
|
||||
export let params: RouteParams
|
||||
export let before: (() => any) | null = null
|
||||
|
||||
let loadedComponent = null
|
||||
const __before = async () => (before ? await before() : null)
|
||||
|
||||
onMount(() => {
|
||||
__before()
|
||||
|
||||
if (component instanceof SvelteComponent) {
|
||||
loadedComponent = component
|
||||
return
|
||||
}
|
||||
|
||||
;(component as () => Promise<any>)().then((module) => {
|
||||
loadedComponent = module.default
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if loadedComponent}
|
||||
<svelte:component this={loadedComponent} {params} {location} />
|
||||
{:else if loading}
|
||||
<svelte:component this={loading} {params} {location} />
|
||||
{/if}
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { Route } from 'svelte-routing'
|
||||
import ProtectedRouteGuard from './ProtectedRouteGuard.svelte'
|
||||
|
||||
export let path: string
|
||||
export let fallback = '/'
|
||||
export let allow = true
|
||||
export let component: any = null
|
||||
</script>
|
||||
|
||||
<Route {path} let:params let:location>
|
||||
<ProtectedRouteGuard {allow} {fallback} {location}>
|
||||
{#if component !== null}
|
||||
<svelte:component this={component} {location} {params} />
|
||||
{:else}
|
||||
<slot {params} {location} />
|
||||
{/if}
|
||||
</ProtectedRouteGuard>
|
||||
</Route>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import type { RouteLocation } from 'svelte-routing/types/Route'
|
||||
import Redirect from './Redirect.svelte'
|
||||
|
||||
export let fallback: string
|
||||
export let allow: boolean
|
||||
export let location: RouteLocation
|
||||
</script>
|
||||
|
||||
{#if allow}
|
||||
<slot />
|
||||
{:else}
|
||||
<Redirect to={fallback} replace state={{ from: location.pathname }} />
|
||||
{/if}
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { navigate } from 'svelte-routing'
|
||||
|
||||
export let to: string
|
||||
export let state: any = null
|
||||
export let replace = false
|
||||
|
||||
export let then: (to: string) => any = () => null
|
||||
|
||||
onMount(() => {
|
||||
navigate(to, { replace, state })
|
||||
then(to)
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { ComponentType, SvelteComponentTyped } from 'svelte'
|
||||
import { Route, Router } from 'svelte-routing'
|
||||
import LazyRouteGuard from './LazyRouteGuard.svelte'
|
||||
|
||||
interface Route {
|
||||
path: string
|
||||
component: ComponentType<SvelteComponentTyped<any>> | (() => Promise<any>)
|
||||
before?: () => any
|
||||
layout?: ComponentType<SvelteComponentTyped<any>>
|
||||
loading?: ComponentType<SvelteComponentTyped<any>>
|
||||
}
|
||||
|
||||
export let layout: ComponentType<SvelteComponentTyped<any>> | null = null
|
||||
export let loading: ComponentType<SvelteComponentTyped<any>> | null = null
|
||||
export let error: ComponentType<SvelteComponentTyped<any>> | null = null
|
||||
export let routes: Route[] = []
|
||||
</script>
|
||||
|
||||
<Router>
|
||||
{#each routes as route}
|
||||
<Route path={route.path} let:location let:params>
|
||||
{#if route?.layout || layout}
|
||||
<svelte:component this={route?.layout ?? layout}>
|
||||
<LazyRouteGuard {location} {params} before={route?.before} loading={route?.loading ?? loading} component={route.component} />
|
||||
</svelte:component>
|
||||
{:else}
|
||||
<LazyRouteGuard {location} {params} before={route?.before} loading={route?.loading ?? loading} component={route.component} />
|
||||
{/if}
|
||||
</Route>
|
||||
{#if error}
|
||||
<Route path="/*" component={error} />
|
||||
{/if}
|
||||
{/each}
|
||||
</Router>
|
|
@ -0,0 +1,9 @@
|
|||
import { navigate as nav } from 'svelte-routing'
|
||||
|
||||
export const navigate = (to: string | number, options?: { replace?: boolean, state?: { [k in string | number]: unknown } }) => {
|
||||
if (typeof to === 'string') return nav(to, options)
|
||||
window.history.go(to)
|
||||
}
|
||||
|
||||
export const back = () => window.history.back()
|
||||
export const forward = () => window.history.forward()
|
|
@ -0,0 +1,29 @@
|
|||
import { ComponentType, SvelteComponentTyped } from 'svelte'
|
||||
|
||||
export interface Route {
|
||||
component: () => Promise<any>,
|
||||
path: string,
|
||||
layout?: ComponentType<SvelteComponentTyped<any>>,
|
||||
loading?: ComponentType<SvelteComponentTyped<any>>,
|
||||
before?: () => any,
|
||||
}
|
||||
|
||||
interface RouteDefinition {
|
||||
component: () => Promise<any>,
|
||||
path: string,
|
||||
layout: ComponentType<SvelteComponentTyped<any>> | null,
|
||||
loading: ComponentType<SvelteComponentTyped<any>> | null,
|
||||
before: () => any | null,
|
||||
}
|
||||
|
||||
interface RouteConfig {
|
||||
routes: Route[],
|
||||
layout?: ComponentType<SvelteComponentTyped<any>> | null,
|
||||
loading?: ComponentType<SvelteComponentTyped<any>> | null,
|
||||
}
|
||||
|
||||
const defineRoutes = (config: RouteConfig): RouteDefinition[] => {
|
||||
return config.routes.map(route => ({ ...route, layout: route?.layout ?? config?.layout ?? null, loading: route?.loading ?? config?.loading ?? null, before: route?.before ?? null }))
|
||||
}
|
||||
|
||||
export default defineRoutes
|
|
@ -0,0 +1,186 @@
|
|||
import { storage, client } from './appwrite'
|
||||
import { ID, Models, RealtimeResponseEvent } from 'appwrite'
|
||||
import { Writable, writable } from 'svelte/store'
|
||||
|
||||
class Bucket {
|
||||
constructor(protected bucketId: string) { }
|
||||
|
||||
createFile(file, permissions: string[] = []) {
|
||||
return storage.createFile(this.bucketId, ID.unique(), file, permissions)
|
||||
}
|
||||
|
||||
deleteFile(file: string | Models.File) {
|
||||
return storage.deleteFile(this.bucketId, typeof file === 'string' ? file : file.$id)
|
||||
}
|
||||
|
||||
updateFile(file: string | Models.File, permissions: string[] = []) {
|
||||
return storage.updateFile(this.bucketId, typeof file === 'string' ? file : file.$id, permissions)
|
||||
}
|
||||
|
||||
getFilePreview(file: string | Models.File) {
|
||||
return storage.getFilePreview(this.bucketId, typeof file === 'string' ? file : file.$id)
|
||||
}
|
||||
|
||||
getFileDownload(file: string | Models.File) {
|
||||
return storage.getFileDownload(this.bucketId, typeof file === 'string' ? file : file.$id)
|
||||
}
|
||||
|
||||
getFileView(file: string | Models.File) {
|
||||
return storage.getFileView(this.bucketId, typeof file === 'string' ? file : file.$id)
|
||||
}
|
||||
|
||||
getFileContent(file: string | Models.File) {
|
||||
const fileContent = writable('')
|
||||
const loading = writable(true)
|
||||
|
||||
const { href } = storage.getFileView(this.bucketId, typeof file === 'string' ? file : file.$id)
|
||||
|
||||
this.subscribeFileUpdateCallback(file, () => fetch(href).then(res => res.ok ? res.text() : null).then(res => {
|
||||
fileContent.set(res ?? '')
|
||||
loading.set(false)
|
||||
}))
|
||||
|
||||
fetch(href).then(res => res.ok ? res.text() : null).then(res => {
|
||||
fileContent.set(res ?? '')
|
||||
loading.set(false)
|
||||
})
|
||||
|
||||
return [{ subscribe: fileContent.subscribe }, { subscribe: loading.subscribe }] as const
|
||||
}
|
||||
|
||||
createUploadDispatcher(acceptManyFiles = false) {
|
||||
let files = []
|
||||
|
||||
const eventUploadDirective = (node: HTMLInputElement) => {
|
||||
const eventListener = (e) => files = acceptManyFiles ? Array.from(e.target.files) : [e.target.files[0]]
|
||||
|
||||
node.addEventListener('change', eventListener)
|
||||
|
||||
acceptManyFiles && node.setAttribute('multiple', 'multiple')
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('change', eventListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dispatchUpload = (permissions: string[] = []) => {
|
||||
return Promise.all(files.map(file => this.createFile(file, permissions)))
|
||||
}
|
||||
|
||||
return [eventUploadDirective, dispatchUpload] as const
|
||||
}
|
||||
|
||||
createSubsciber(queries: string[] = [], search = '') {
|
||||
const filesStore = writable<Models.File[]>([])
|
||||
const loadingStore = writable(true)
|
||||
|
||||
storage.listFiles(this.bucketId, queries, search).then(({ files }) => {
|
||||
files.forEach(file => this.subscribeFileUpdate(file, filesStore))
|
||||
filesStore.set(files)
|
||||
loadingStore.set(false)
|
||||
})
|
||||
|
||||
return [{ subscribe: filesStore.subscribe }, { subscribe: loadingStore.subscribe }] as const
|
||||
}
|
||||
|
||||
createObserver() {
|
||||
const dataStore = writable<Models.File[]>([])
|
||||
|
||||
client.subscribe(`buckets.${this.bucketId}.files`, (response: RealtimeResponseEvent<any>) => {
|
||||
if (response.events.includes(`buckets.${this.bucketId}.files.*.create`)) {
|
||||
dataStore.update(current => {
|
||||
current.push(response.payload)
|
||||
return current
|
||||
})
|
||||
|
||||
this.subscribeFileUpdate(response.payload, dataStore)
|
||||
}
|
||||
})
|
||||
|
||||
return { subscribe: dataStore.subscribe }
|
||||
}
|
||||
|
||||
protected subscribeFileUpdate(file: Models.File, filesStore: Writable<Models.File[]>) {
|
||||
this.subscribeFileUpdateCallback(file, ({ event }) => {
|
||||
if (event === 'update') return filesStore.update(current => {
|
||||
current[current.indexOf(file)] = file
|
||||
return current
|
||||
})
|
||||
|
||||
filesStore.update(current => {
|
||||
current.splice(current.indexOf(file), 1)
|
||||
return current
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
protected subscribeFileUpdateCallback(file: string | Models.File, callback: ({ fileId, event }: { fileId: string, event: 'update' | 'delete' }) => any) {
|
||||
client.subscribe(`buckets.${this.bucketId}.files.${typeof file === 'string' ? file : file.$id}`, (response: RealtimeResponseEvent<any>) => {
|
||||
if (response.events.includes(`buckets.${this.bucketId}.files.${typeof file === 'string' ? file : file.$id}.update`)) {
|
||||
return callback({ fileId: typeof file === 'string' ? file : file.$id, event: 'update' })
|
||||
}
|
||||
|
||||
if (response.events.includes(`buckets.${this.bucketId}.files.${typeof file === 'string' ? file : file.$id}.delete`)) {
|
||||
return callback({ fileId: typeof file === 'string' ? file : file.$id, event: 'delete' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class File {
|
||||
constructor(protected bucketId: string, protected fileId: string) { }
|
||||
|
||||
createSubscriber() {
|
||||
const fileStore = writable<Models.File>(null)
|
||||
const loadingStore = writable(true)
|
||||
|
||||
storage.getFile(this.bucketId, this.fileId).then((result) => {
|
||||
fileStore.set(result)
|
||||
loadingStore.set(false)
|
||||
})
|
||||
|
||||
client.subscribe(`buckets.${this.bucketId}.files.${this.fileId}`, (response: RealtimeResponseEvent<any>) => {
|
||||
if (response.events.includes(`buckets.${this.bucketId}.files.${this.fileId}.update`)) {
|
||||
fileStore.set(response.payload)
|
||||
return
|
||||
}
|
||||
|
||||
if (response.events.includes(`buckets.${this.bucketId}.files.${this.fileId}.delete`)) {
|
||||
fileStore.set(null)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return [{ subscribe: fileStore.subscribe }, { subscribe: loadingStore.subscribe }] as const
|
||||
}
|
||||
|
||||
delete() {
|
||||
return storage.deleteFile(this.bucketId, this.fileId)
|
||||
}
|
||||
|
||||
update(permissions: string[] = []) {
|
||||
return storage.updateFile(this.bucketId, this.fileId, permissions)
|
||||
}
|
||||
|
||||
getPreview() {
|
||||
return storage.getFilePreview(this.bucketId, this.fileId)
|
||||
}
|
||||
|
||||
getDownload() {
|
||||
return storage.getFileDownload(this.bucketId, this.fileId)
|
||||
}
|
||||
|
||||
getView() {
|
||||
return storage.getFileView(this.bucketId, this.fileId)
|
||||
}
|
||||
|
||||
async getContent() {
|
||||
const { href } = await storage.getFileView(this.bucketId, this.fileId)
|
||||
|
||||
return await fetch(href)
|
||||
}
|
||||
}
|
||||
|
||||
export { Bucket, File }
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"page": {
|
||||
"home": {
|
||||
"title": "Appwrite svelte rocket start 🚀"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { register, init, getLocaleFromNavigator, isLoading, locale, locales } from 'svelte-i18n'
|
||||
import registers from './languages'
|
||||
|
||||
Object.entries(registers).forEach(([key, file]) => register(key, file))
|
||||
|
||||
export const i18n = () => init({
|
||||
fallbackLocale: 'en',
|
||||
initialLocale: getLocaleFromNavigator(),
|
||||
})
|
||||
|
||||
export { isLoading, locale, locales }
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
'en': () => import('./en.json')
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app')
|
||||
})
|
||||
|
||||
export default app
|
|
@ -0,0 +1,16 @@
|
|||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
tab-size: 4;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import Link from '$lib/components/Link.svelte'
|
||||
import { _ } from 'svelte-i18n'
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center mt-20">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
||||
<span class="block text-indigo-600">{$_('page.home.title')}</span>
|
||||
<p>
|
||||
<Link class="underline" href="https://appwrite.io/">Appwrite</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link class="underline" href="/oauth">OAuth</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link class="underline" href="https://github.com/lewis-wow/appwrite-svelte-rocket-start">Repository</Link>
|
||||
</p>
|
||||
</h1>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
<h1>OAuth failed</h1>
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
import { account, url } from '$lib/appwrite'
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<button on:click={() => account.createOAuth2Session('github', url.oauth.success, url.oauth.failure)}> Github </button>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { navigate } from '$lib/router'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
onMount(() => {
|
||||
const currentTitle = document.title
|
||||
document.title = 'Authorizing...'
|
||||
|
||||
navigate('/', { replace: true })
|
||||
return () => (document.title = currentTitle)
|
||||
})
|
||||
</script>
|
||||
|
||||
<p>Authorizing...</p>
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
|
@ -0,0 +1,15 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
'./cms/**/*.{html,js,svelte,ts}',
|
||||
'./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}',
|
||||
],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: [
|
||||
require('flowbite/plugin')
|
||||
],
|
||||
darkMode: 'class'
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"$lib/*": [
|
||||
"./src/lib/*"
|
||||
],
|
||||
"$root/*": [
|
||||
"./*"
|
||||
],
|
||||
"$src/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"$cms/*": [
|
||||
"./cms/*"
|
||||
],
|
||||
"$routes/*": [
|
||||
"./src/routes/*"
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import preprocess from "svelte-preprocess"
|
||||
import path from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'$lib': path.resolve(__dirname, 'src', 'lib'),
|
||||
'$root': path.resolve(__dirname),
|
||||
'$src': path.resolve(__dirname, 'src'),
|
||||
'$cms': path.resolve(__dirname, 'src', 'cms'),
|
||||
'$routes': path.resolve(__dirname, 'src', 'routes')
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
preprocess: preprocess({
|
||||
scss: true,
|
||||
postcss: true
|
||||
})
|
||||
})
|
||||
]
|
||||
})
|
Loading…
Reference in New Issue