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