├── .eslintignore ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── .pnpm-debug.log ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __.eslintrc.cjs ├── eslint.config.js ├── jsconfig.json ├── openapi.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── app.css ├── app.d.ts ├── app.html ├── components │ ├── CategoryQuestions │ │ ├── MenuQuestion.svelte │ │ ├── OptionsModal.svelte │ │ ├── Questions.svelte │ │ └── TextQuestion.svelte │ ├── DataModal.svelte │ ├── ErrorBox.svelte │ ├── ErrorPage.svelte │ ├── ImportModal.svelte │ ├── Required.svelte │ ├── ResetModal.svelte │ ├── Spinner.svelte │ ├── TagInputs.svelte │ ├── TopBar.svelte │ ├── Tree.svelte │ ├── WelcomeModal.svelte │ └── state.svelte.js ├── hooks.client.js ├── hooks.server.js ├── lib │ ├── i18n.js │ ├── locales │ │ └── en-GB │ │ │ ├── _common.json │ │ │ └── misc.json │ ├── timezones.json │ └── util │ │ └── data.js └── routes │ ├── (default) │ ├── +error.svelte │ ├── +layout.svelte │ ├── +page.js │ ├── +page.svelte │ ├── [guild] │ │ ├── +layout.js │ │ ├── +layout.svelte │ │ ├── +page.js │ │ ├── +page.svelte │ │ ├── feedback │ │ │ └── +page.svelte │ │ ├── staff │ │ │ └── +page.svelte │ │ └── tickets │ │ │ └── +page.svelte │ ├── invite │ │ └── +page.js │ ├── login │ │ ├── +page.js │ │ └── +page.svelte │ └── view │ │ └── [ticket] │ │ └── +page.svelte │ ├── +layout.server.js │ ├── +layout.svelte │ └── settings │ ├── +error.svelte │ ├── +layout.svelte │ ├── +page.js │ ├── +page.svelte │ └── [guild] │ ├── +layout.js │ ├── +layout.svelte │ ├── +page.js │ ├── +page@settings.svelte │ ├── categories │ ├── +page.js │ ├── +page.svelte │ └── [category] │ │ ├── +page.js │ │ └── +page.svelte │ ├── feedback │ └── +page.svelte │ ├── general │ ├── +page.js │ └── +page.svelte │ ├── panels │ ├── +page.js │ └── +page.svelte │ └── tags │ ├── +page.js │ └── +page.svelte ├── static ├── assets │ ├── topgg-dark.webp │ ├── topgg-light.webp │ ├── undraw_reviews.svg │ ├── wordmark-dark.png │ └── wordmark-light.png └── favicon.png ├── svelte.config.js ├── swagger.json ├── tailwind.config.js ├── tsconfig.json └── vite.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint, test, and build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | env: 10 | PUBLIC_HOST: http://localhost 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: '18' 19 | - name: Cache pnpm modules 20 | uses: actions/cache@v2 21 | env: 22 | cache-name: cache-pnpm-modules 23 | with: 24 | path: ~/.pnpm-store 25 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}- 28 | - uses: pnpm/action-setup@v2.0.1 29 | with: 30 | version: 7.9.0 31 | run_install: false 32 | - run: pnpm install 33 | - run: pnpm run lint 34 | - run: pnpm run build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.pnpm-debug.log: -------------------------------------------------------------------------------- 1 | { 2 | "0 debug pnpm:scope": { 3 | "selected": 1 4 | }, 5 | "1 error pnpm": { 6 | "code": "ERR_PNPM_INIT_ARG", 7 | "hint": "Maybe you wanted to run \"pnpm create svelte\"", 8 | "err": { 9 | "name": "pnpm", 10 | "message": "init command does not accept any arguments", 11 | "code": "ERR_PNPM_INIT_ARG", 12 | "stack": "pnpm: init command does not accept any arguments\n at Object.handler [as init] (/home/isaac/.nvm/versions/node/v17.6.0/pnpm-global/5/node_modules/.pnpm/pnpm@7.1.2/node_modules/pnpm/dist/pnpm.cjs:173945:15)\n at /home/isaac/.nvm/versions/node/v17.6.0/pnpm-global/5/node_modules/.pnpm/pnpm@7.1.2/node_modules/pnpm/dist/pnpm.cjs:176459:51\n at async run (/home/isaac/.nvm/versions/node/v17.6.0/pnpm-global/5/node_modules/.pnpm/pnpm@7.1.2/node_modules/pnpm/dist/pnpm.cjs:176435:34)\n at async runPnpm (/home/isaac/.nvm/versions/node/v17.6.0/pnpm-global/5/node_modules/.pnpm/pnpm@7.1.2/node_modules/pnpm/dist/pnpm.cjs:176653:5)\n at async /home/isaac/.nvm/versions/node/v17.6.0/pnpm-global/5/node_modules/.pnpm/pnpm@7.1.2/node_modules/pnpm/dist/pnpm.cjs:176645:7" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["cooldown", "skyra", "uuidv"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Tickets Portal 2 | 3 | A web app for interacting with the [Discord Tickets](https://discordtickets.app) bot via the API. 4 | 5 | > [!NOTE] 6 | > 7 | > This is bundled with the bot; you don't need to download and install it separately. 8 | 9 | ## Screenshot 10 | 11 | 12 | 13 | 14 | A screenshot of the welcome screen 15 | 16 | -------------------------------------------------------------------------------- /__.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['eslint:recommended', 'plugin:svelte/recommended', 'prettier'], 4 | parserOptions: { 5 | sourceType: 'module', 6 | ecmaVersion: 2020, 7 | extraFileExtensions: ['.svelte'], 8 | tsconfigRootDir: __dirname 9 | }, 10 | env: { 11 | browser: true, 12 | es2017: true, 13 | node: true 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | 6 | /** @type {import('eslint').Linter.Config[]} */ 7 | export default [ 8 | js.configs.recommended, 9 | ...svelte.configs['flat/recommended'], 10 | prettier, 11 | ...svelte.configs['flat/prettier'], 12 | { 13 | languageOptions: { 14 | globals: { 15 | ...globals.browser, 16 | ...globals.node 17 | } 18 | } 19 | }, 20 | { 21 | ignores: ['build/', '.svelte-kit/', 'dist/'] 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Discord Tickets API", 5 | "description": "This is the schema for the API that you can use to interact with your ticket bot. It is used by the Archives Portal and the Settings Panel websites.\nIf you are using a managed ticket bot your API will be available at `https://hosting.discordtickets.app:{port}`. Create a ticket if you don't know what port your bot is on.\n# Error codes\n- `0x191`: Unauthorised\n- `0x193`: Forbidden", 6 | "version": "4.0.0" 7 | }, 8 | "servers": [ 9 | { 10 | "url": "https://virtserver.swaggerhub.com/eartharoid/discord-tickets/4.0.0", 11 | "description": "[TEST] SwaggerHub API Auto Mocking" 12 | }, 13 | { 14 | "url": "http://localhost:{port}/api", 15 | "description": "[DEV] Local development API server", 16 | "variables": { 17 | "port": { 18 | "default": "8080" 19 | } 20 | } 21 | }, 22 | { 23 | "url": "{host}/api", 24 | "description": "[PROD] Production API server", 25 | "variables": { 26 | "host": { 27 | "default": "http://localhost:8080" 28 | } 29 | } 30 | } 31 | ], 32 | "security": [ 33 | { 34 | "api_key": [] 35 | } 36 | ], 37 | "paths": { 38 | "/admin/guilds": { 39 | "get": { 40 | "tags": ["admin"], 41 | "summary": "List the guilds that the user has permission to manage", 42 | "description": "This route returns an array of guilds that the user can manage.", 43 | "responses": { 44 | "200": { 45 | "description": "OK", 46 | "content": { 47 | "application/json": { 48 | "schema": { 49 | "type": "array", 50 | "items": { 51 | "$ref": "#/components/schemas/Guild" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "/admin/guilds/{guild}": { 61 | "get": { 62 | "tags": ["admin"], 63 | "summary": "Get a guild's settings", 64 | "description": "This route returns the guild's current settings.", 65 | "parameters": [ 66 | { 67 | "name": "guild", 68 | "in": "path", 69 | "description": "The guild to get the settings of", 70 | "required": true, 71 | "style": "simple", 72 | "explode": false, 73 | "schema": { 74 | "type": "string" 75 | } 76 | } 77 | ], 78 | "responses": { 79 | "200": { 80 | "description": "OK", 81 | "content": { 82 | "application/json": { 83 | "schema": { 84 | "$ref": "#/components/schemas/GuildSettings" 85 | } 86 | } 87 | } 88 | } 89 | } 90 | }, 91 | "put": { 92 | "tags": ["admin"], 93 | "summary": "Set a guild's settings", 94 | "description": "This route sets the guild's settings.", 95 | "parameters": [ 96 | { 97 | "name": "guild", 98 | "in": "path", 99 | "description": "The guild to update the settings of", 100 | "required": true, 101 | "style": "simple", 102 | "explode": false, 103 | "schema": { 104 | "type": "string" 105 | } 106 | } 107 | ], 108 | "responses": { 109 | "200": { 110 | "description": "OK", 111 | "content": { 112 | "application/json": { 113 | "schema": { 114 | "$ref": "#/components/schemas/GuildSettings" 115 | } 116 | } 117 | } 118 | }, 119 | "401": { 120 | "description": "Unauthorised", 121 | "content": { 122 | "application/json": { 123 | "schema": { 124 | "$ref": "#/components/schemas/Error" 125 | } 126 | } 127 | } 128 | }, 129 | "403": { 130 | "description": "Forbidden", 131 | "content": { 132 | "application/json": { 133 | "schema": { 134 | "$ref": "#/components/schemas/Error" 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | }, 142 | "/archives/guilds": { 143 | "get": { 144 | "tags": ["user"], 145 | "summary": "List the guilds that the client and the user have in common", 146 | "description": "This route returns an array of guilds that the client and user have in common and it is therefore possible for the user to have a ticket in.", 147 | "responses": { 148 | "200": { 149 | "description": "OK", 150 | "content": { 151 | "application/json": { 152 | "schema": { 153 | "type": "array", 154 | "items": { 155 | "$ref": "#/components/schemas/Guild" 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | }, 165 | "components": { 166 | "schemas": { 167 | "Error": { 168 | "type": "object", 169 | "properties": { 170 | "code": { 171 | "type": "number" 172 | }, 173 | "message": { 174 | "type": "string" 175 | } 176 | }, 177 | "example": { 178 | "code": 1, 179 | "message": "Unauthorised" 180 | } 181 | }, 182 | "Guild": { 183 | "type": "object", 184 | "properties": { 185 | "id": { 186 | "type": "string" 187 | }, 188 | "logo": { 189 | "type": "string" 190 | }, 191 | "name": { 192 | "type": "string" 193 | } 194 | }, 195 | "example": { 196 | "id": "451745464480432129", 197 | "logo": "https://cdn.discordapp.com/icons/451745464480432129/c340066806e27569c1c6b2bbd8ab28f1.png", 198 | "name": "Planet Earth" 199 | } 200 | }, 201 | "GuildSettings": { 202 | "type": "object", 203 | "properties": { 204 | "archive": { 205 | "type": "boolean" 206 | }, 207 | "errorColour": { 208 | "type": "string" 209 | }, 210 | "primaryColour": { 211 | "type": "string" 212 | }, 213 | "successColour": { 214 | "type": "string" 215 | } 216 | }, 217 | "example": { 218 | "archive": true, 219 | "errorColour": "RED", 220 | "primaryColour": "#009999", 221 | "successColour": "GREEN" 222 | } 223 | } 224 | }, 225 | "securitySchemes": { 226 | "api_key": { 227 | "type": "apiKey", 228 | "name": "Authorization", 229 | "in": "header" 230 | } 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discord-tickets/settings", 3 | "private": false, 4 | "version": "2.5.4", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "files": [ 8 | "build/" 9 | ], 10 | "scripts": { 11 | "dev": "vite dev", 12 | "build": "vite build", 13 | "prepack": "npm run build", 14 | "preview": "vite preview", 15 | "lint": "prettier --check --plugin-search-dir=. . && eslint .", 16 | "format": "prettier --write --plugin-search-dir=. ." 17 | }, 18 | "devDependencies": { 19 | "@eartharoid/i18n": "2.0.0-alpha.1", 20 | "@eartharoid/vite-plugin-i18n": "1.0.0-alpha.1", 21 | "@fortawesome/fontawesome-free": "^6.6.0", 22 | "@skyra/discord-components-core": "^3.6.1", 23 | "@sveltejs/adapter-node": "^5.2.9", 24 | "@sveltejs/kit": "^2.17.1", 25 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 26 | "@tailwindcss/forms": "^0.5.9", 27 | "@tailwindcss/typography": "^0.5.15", 28 | "autoprefixer": "^10.4.20", 29 | "big-integer": "^1.6.52", 30 | "clsx": "^2.1.1", 31 | "cookie": "^0.5.0", 32 | "emoji-name-map": "^1.2.9", 33 | "eslint": "^8.57.1", 34 | "eslint-config-prettier": "^8.10.0", 35 | "eslint-plugin-svelte": "^2.46.0", 36 | "jszip": "^3.10.1", 37 | "marked": "^4.3.0", 38 | "ms": "^2.1.3", 39 | "negotiator": "^0.6.4", 40 | "postcss": "^8.4.49", 41 | "prettier": "^3.4.2", 42 | "prettier-plugin-svelte": "^3.3.3", 43 | "prettier-plugin-tailwindcss": "^0.6.11", 44 | "sortablejs": "^1.15.3", 45 | "svelte": "^5.19.9", 46 | "svelte-modals": "^2.0.0", 47 | "svelte-multiselect": "11.0.0-rc.1", 48 | "svelte-preprocess": "^6.0.3", 49 | "svelte-toasts": "^1.1.2", 50 | "tailwind-merge": "^2.5.4", 51 | "tailwind-variants": "^0.2.1", 52 | "tailwindcss": "^3.4.15", 53 | "typescript": "^5.6.3", 54 | "uuid": "^9.0.1", 55 | "vite": "^6.1.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | background: #2f3136; 8 | scroll-behavior: smooth; 9 | } 10 | } 11 | 12 | @layer components { 13 | .link { 14 | @apply transition duration-300 hover:bg-blurple hover:text-white dark:hover:bg-blurple dark:hover:text-white; 15 | } 16 | 17 | .input { 18 | /* focus: bg-white */ 19 | @apply my-1 block w-full rounded-md border-transparent bg-gray-100 font-normal shadow-sm transition-colors placeholder:text-gray-500 focus:ring-2 focus:ring-blurple disabled:cursor-not-allowed dark:bg-slate-800 placeholder:dark:text-slate-400; 20 | } 21 | 22 | .form-checkbox { 23 | @apply m-2 cursor-pointer rounded bg-gray-100 p-3 text-blurple checked:bg-blurple focus:ring-blurple disabled:cursor-not-allowed dark:bg-slate-800 dark:checked:bg-blurple; 24 | } 25 | 26 | select option:checked, 27 | select option:checked i { 28 | @apply bg-blurple text-white; 29 | } 30 | 31 | .dragged { 32 | @apply bg-blurple/10 dark:bg-blurple/10; 33 | } 34 | 35 | .marked { 36 | @apply inline-block; 37 | } 38 | } 39 | 40 | /* 41 | ? MODALS 42 | */ 43 | 44 | .backdrop { 45 | position: fixed; 46 | top: 0; 47 | bottom: 0; 48 | right: 0; 49 | left: 0; 50 | background: rgba(0, 0, 0, 0.5); 51 | } 52 | 53 | .modal { 54 | position: fixed; 55 | top: 0; 56 | bottom: 0; 57 | right: 0; 58 | left: 0; 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | /* allow click-through to backdrop */ 63 | pointer-events: none; 64 | } 65 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/CategoryQuestions/MenuQuestion.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 36 |
37 |
38 | 53 |
54 |
55 | 71 |
72 |
73 |
74 | Options ({question.options.length}/25) 75 | 76 | 80 | 88 |
89 |
90 | 95 |
96 |
97 |
98 |
99 | 112 |
113 |
114 | -------------------------------------------------------------------------------- /src/components/CategoryQuestions/OptionsModal.svelte: -------------------------------------------------------------------------------- 1 | 223 | -------------------------------------------------------------------------------- /src/components/CategoryQuestions/Questions.svelte: -------------------------------------------------------------------------------- 1 | 58 | 59 |
60 | {#each qS.questions as q, i} 61 |
62 |
63 |
64 | 66 | 67 |
68 | {q.label} 69 | 82 | 94 |
95 |
96 | {#if expanded === q.id} 97 |
98 |
99 |
100 | 128 |
129 | 130 | {#if q.type === 'TEXT'} 131 | 132 | {:else if q.type === 'MENU'} 133 | 134 | {/if} 135 |
136 |
137 | {/if} 138 |
139 |
140 | {/each} 141 |
142 | -------------------------------------------------------------------------------- /src/components/CategoryQuestions/TextQuestion.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 22 |
23 |
24 | 39 |
40 |
41 | 56 |
57 |
58 | 71 |
72 |
73 | 87 |
88 |
89 | 106 |
107 |
108 | 117 |
118 | -------------------------------------------------------------------------------- /src/components/DataModal.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if isOpen} 14 | 87 | {/if} 88 | -------------------------------------------------------------------------------- /src/components/ErrorBox.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 |

13 | Sorry, something went wrong. 14 |

15 |
16 |

17 | Error 18 |

19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/components/ErrorPage.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 |
14 |

15 | Sorry, something went wrong. 16 |

17 |

18 | Your request failed with HTTP status 19 | {$page.status}. 20 |

21 |
22 |
23 |

24 | URL: 25 | {$page.url} 26 |

27 |

28 | Route: 29 | {$page.route.id} 30 |

31 |
32 |
33 |

34 | Error 35 |

36 | 37 |
38 | {#if $page.params && Object.keys($page.params).length > 0} 39 |
40 |

41 | Parameters 42 |

43 | 44 |
45 | {/if} 46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /src/components/ImportModal.svelte: -------------------------------------------------------------------------------- 1 | 82 | 83 | {#snippet warning(message)} 84 |
87 |
88 | 89 |
90 | {message} 91 |
92 |
93 |
94 | {/snippet} 95 | 96 | {#if isOpen} 97 | 277 | {/if} 278 | -------------------------------------------------------------------------------- /src/components/Required.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/ResetModal.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | {#snippet warning(message)} 25 |
28 |
29 | 30 |
31 | {message} 32 |
33 |
34 |
35 | {/snippet} 36 | 37 | {#if isOpen} 38 | 95 | {/if} 96 | -------------------------------------------------------------------------------- /src/components/Spinner.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 10 | 81 | -------------------------------------------------------------------------------- /src/components/TagInputs.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 19 |
20 |
21 | 29 |
30 |
31 | 41 | {#if tag.content} 42 |

Preview

43 |
46 | {@html marked.parse( 47 | tag.content 48 | .replace(/\n/g, '\n\n') 49 | .replace(/{+\s?(user)?name\s?}+/gi, '@' + getContext('user').username) 50 | )} 51 |
52 | {/if} 53 |
54 | -------------------------------------------------------------------------------- /src/components/TopBar.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | Discord Tickets 24 | 25 |
26 |
27 |
30 | 35 | Discord Tickets 40 | {user.username} 41 | 42 |
43 | {#if theme === 'dark'} 44 | toggle()} 48 | > 49 | {:else} 50 | toggle()} 54 | > 55 | {/if} 56 |
57 |
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /src/components/Tree.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {#if entry instanceof Array} 11 | {#each entry as child} 12 |
0} class="font-mono" style="padding-left: {indent}px;"> 13 | 14 |
17 |

{child[0]}

18 | 19 |
20 |
21 | {/each} 22 | {:else} 23 | 24 |

25 | {@html marked.parse(entry)} 26 |

27 | {/if} 28 |
29 | -------------------------------------------------------------------------------- /src/components/WelcomeModal.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#if isOpen} 16 | 167 | {/if} 168 | -------------------------------------------------------------------------------- /src/components/state.svelte.js: -------------------------------------------------------------------------------- 1 | export const questionsState = $state({ 2 | questions: [] 3 | }); 4 | 5 | export const tagsState = $state({ 6 | tags: [] 7 | }); 8 | -------------------------------------------------------------------------------- /src/hooks.client.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@sveltejs/kit').Handle} */ 2 | export async function handle({ event, resolve }) { 3 | const response = await resolve(event, { 4 | filterSerializedResponseHeaders: () => true 5 | }); 6 | return response; 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks.server.js: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | 3 | /** @type {import('@sveltejs/kit').Handle} */ 4 | export async function handle({ event, resolve }) { 5 | const response = await resolve(event, { 6 | filterSerializedResponseHeaders: () => true 7 | }); 8 | return response; 9 | } 10 | 11 | /** @type {import('@sveltejs/kit').HandleServerError} */ 12 | export function handleError({ error, event }) { 13 | const errorId = Date.now().toString(16); 14 | if (dev || process?.env.NODE_ENV === 'development') console.error(error); 15 | process?.emit('sveltekit:error', { error, errorId, event }); 16 | return { 17 | name: 'Internal Server Error', 18 | message: error.message, 19 | errorId 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/i18n.js: -------------------------------------------------------------------------------- 1 | export const importJSON = (...modules) => [ 2 | modules[0].locale_id, 3 | [].concat(...modules.map((mod) => mod.json)) 4 | ]; 5 | 6 | export const getSupportedLocales = () => { 7 | const files = Object.keys(import.meta.glob('$lib/locales/**')); 8 | return Array.from( 9 | new Set( 10 | files.map((file) => { 11 | const parts = file.split('/'); 12 | return parts[parts.length - 2]; 13 | }) 14 | ) 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/locales/en-GB/_common.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Language", 3 | "logout": "Log out", 4 | "settings_panel": "Settings Panel", 5 | "staff_dashboard": "Staff Dashboard", 6 | "theme": "Theme", 7 | "title": "{guild} - {client} Portal" 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/locales/en-GB/misc.json: -------------------------------------------------------------------------------- 1 | { 2 | "login_title": "Log in - {username} Portal", 3 | "please_login": "Log in to the Portal to view this page", 4 | "select_server": "Select a server", 5 | "select_server_title": "{username} Portal", 6 | "continue_with_discord": "Continue with Discord" 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/timezones.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Africa/Abidjan", 3 | "Africa/Accra", 4 | "Africa/Addis_Ababa", 5 | "Africa/Algiers", 6 | "Africa/Asmara", 7 | "Africa/Asmera", 8 | "Africa/Bamako", 9 | "Africa/Bangui", 10 | "Africa/Banjul", 11 | "Africa/Bissau", 12 | "Africa/Blantyre", 13 | "Africa/Brazzaville", 14 | "Africa/Bujumbura", 15 | "Africa/Cairo", 16 | "Africa/Casablanca", 17 | "Africa/Ceuta", 18 | "Africa/Conakry", 19 | "Africa/Dakar", 20 | "Africa/Dar_es_Salaam", 21 | "Africa/Djibouti", 22 | "Africa/Douala", 23 | "Africa/El_Aaiun", 24 | "Africa/Freetown", 25 | "Africa/Gaborone", 26 | "Africa/Harare", 27 | "Africa/Johannesburg", 28 | "Africa/Juba", 29 | "Africa/Kampala", 30 | "Africa/Khartoum", 31 | "Africa/Kigali", 32 | "Africa/Kinshasa", 33 | "Africa/Lagos", 34 | "Africa/Libreville", 35 | "Africa/Lome", 36 | "Africa/Luanda", 37 | "Africa/Lubumbashi", 38 | "Africa/Lusaka", 39 | "Africa/Malabo", 40 | "Africa/Maputo", 41 | "Africa/Maseru", 42 | "Africa/Mbabane", 43 | "Africa/Mogadishu", 44 | "Africa/Monrovia", 45 | "Africa/Nairobi", 46 | "Africa/Ndjamena", 47 | "Africa/Niamey", 48 | "Africa/Nouakchott", 49 | "Africa/Ouagadougou", 50 | "Africa/Porto-Novo", 51 | "Africa/Sao_Tome", 52 | "Africa/Timbuktu", 53 | "Africa/Tripoli", 54 | "Africa/Tunis", 55 | "Africa/Windhoek", 56 | "America/Adak", 57 | "America/Anchorage", 58 | "America/Anguilla", 59 | "America/Antigua", 60 | "America/Araguaina", 61 | "America/Argentina/Buenos_Aires", 62 | "America/Argentina/Catamarca", 63 | "America/Argentina/ComodRivadavia", 64 | "America/Argentina/Cordoba", 65 | "America/Argentina/Jujuy", 66 | "America/Argentina/La_Rioja", 67 | "America/Argentina/Mendoza", 68 | "America/Argentina/Rio_Gallegos", 69 | "America/Argentina/Salta", 70 | "America/Argentina/San_Juan", 71 | "America/Argentina/San_Luis", 72 | "America/Argentina/Tucuman", 73 | "America/Argentina/Ushuaia", 74 | "America/Aruba", 75 | "America/Asuncion", 76 | "America/Atikokan", 77 | "America/Atka", 78 | "America/Bahia", 79 | "America/Bahia_Banderas", 80 | "America/Barbados", 81 | "America/Belem", 82 | "America/Belize", 83 | "America/Blanc-Sablon", 84 | "America/Boa_Vista", 85 | "America/Bogota", 86 | "America/Boise", 87 | "America/Buenos_Aires", 88 | "America/Cambridge_Bay", 89 | "America/Campo_Grande", 90 | "America/Cancun", 91 | "America/Caracas", 92 | "America/Catamarca", 93 | "America/Cayenne", 94 | "America/Cayman", 95 | "America/Chicago", 96 | "America/Chihuahua", 97 | "America/Coral_Harbour", 98 | "America/Cordoba", 99 | "America/Costa_Rica", 100 | "America/Creston", 101 | "America/Cuiaba", 102 | "America/Curacao", 103 | "America/Danmarkshavn", 104 | "America/Dawson", 105 | "America/Dawson_Creek", 106 | "America/Denver", 107 | "America/Detroit", 108 | "America/Dominica", 109 | "America/Edmonton", 110 | "America/Eirunepe", 111 | "America/El_Salvador", 112 | "America/Ensenada", 113 | "America/Fort_Nelson", 114 | "America/Fort_Wayne", 115 | "America/Fortaleza", 116 | "America/Glace_Bay", 117 | "America/Godthab", 118 | "America/Goose_Bay", 119 | "America/Grand_Turk", 120 | "America/Grenada", 121 | "America/Guadeloupe", 122 | "America/Guatemala", 123 | "America/Guayaquil", 124 | "America/Guyana", 125 | "America/Halifax", 126 | "America/Havana", 127 | "America/Hermosillo", 128 | "America/Indiana/Indianapolis", 129 | "America/Indiana/Knox", 130 | "America/Indiana/Marengo", 131 | "America/Indiana/Petersburg", 132 | "America/Indiana/Tell_City", 133 | "America/Indiana/Vevay", 134 | "America/Indiana/Vincennes", 135 | "America/Indiana/Winamac", 136 | "America/Indianapolis", 137 | "America/Inuvik", 138 | "America/Iqaluit", 139 | "America/Jamaica", 140 | "America/Jujuy", 141 | "America/Juneau", 142 | "America/Kentucky/Louisville", 143 | "America/Kentucky/Monticello", 144 | "America/Knox_IN", 145 | "America/Kralendijk", 146 | "America/La_Paz", 147 | "America/Lima", 148 | "America/Los_Angeles", 149 | "America/Louisville", 150 | "America/Lower_Princes", 151 | "America/Maceio", 152 | "America/Managua", 153 | "America/Manaus", 154 | "America/Marigot", 155 | "America/Martinique", 156 | "America/Matamoros", 157 | "America/Mazatlan", 158 | "America/Mendoza", 159 | "America/Menominee", 160 | "America/Merida", 161 | "America/Metlakatla", 162 | "America/Mexico_City", 163 | "America/Miquelon", 164 | "America/Moncton", 165 | "America/Monterrey", 166 | "America/Montevideo", 167 | "America/Montreal", 168 | "America/Montserrat", 169 | "America/Nassau", 170 | "America/New_York", 171 | "America/Nipigon", 172 | "America/Nome", 173 | "America/Noronha", 174 | "America/North_Dakota/Beulah", 175 | "America/North_Dakota/Center", 176 | "America/North_Dakota/New_Salem", 177 | "America/Nuuk", 178 | "America/Ojinaga", 179 | "America/Panama", 180 | "America/Pangnirtung", 181 | "America/Paramaribo", 182 | "America/Phoenix", 183 | "America/Port-au-Prince", 184 | "America/Port_of_Spain", 185 | "America/Porto_Acre", 186 | "America/Porto_Velho", 187 | "America/Puerto_Rico", 188 | "America/Punta_Arenas", 189 | "America/Rainy_River", 190 | "America/Rankin_Inlet", 191 | "America/Recife", 192 | "America/Regina", 193 | "America/Resolute", 194 | "America/Rio_Branco", 195 | "America/Rosario", 196 | "America/Santa_Isabel", 197 | "America/Santarem", 198 | "America/Santiago", 199 | "America/Santo_Domingo", 200 | "America/Sao_Paulo", 201 | "America/Scoresbysund", 202 | "America/Shiprock", 203 | "America/Sitka", 204 | "America/St_Barthelemy", 205 | "America/St_Johns", 206 | "America/St_Kitts", 207 | "America/St_Lucia", 208 | "America/St_Thomas", 209 | "America/St_Vincent", 210 | "America/Swift_Current", 211 | "America/Tegucigalpa", 212 | "America/Thule", 213 | "America/Thunder_Bay", 214 | "America/Tijuana", 215 | "America/Toronto", 216 | "America/Tortola", 217 | "America/Vancouver", 218 | "America/Virgin", 219 | "America/Whitehorse", 220 | "America/Winnipeg", 221 | "America/Yakutat", 222 | "America/Yellowknife", 223 | "Antarctica/Casey", 224 | "Antarctica/Davis", 225 | "Antarctica/DumontDUrville", 226 | "Antarctica/Macquarie", 227 | "Antarctica/Mawson", 228 | "Antarctica/McMurdo", 229 | "Antarctica/Palmer", 230 | "Antarctica/Rothera", 231 | "Antarctica/South_Pole", 232 | "Antarctica/Syowa", 233 | "Antarctica/Troll", 234 | "Antarctica/Vostok", 235 | "Arctic/Longyearbyen", 236 | "Asia/Aden", 237 | "Asia/Almaty", 238 | "Asia/Amman", 239 | "Asia/Anadyr", 240 | "Asia/Aqtau", 241 | "Asia/Aqtobe", 242 | "Asia/Ashgabat", 243 | "Asia/Ashkhabad", 244 | "Asia/Atyrau", 245 | "Asia/Baghdad", 246 | "Asia/Bahrain", 247 | "Asia/Baku", 248 | "Asia/Bangkok", 249 | "Asia/Barnaul", 250 | "Asia/Beirut", 251 | "Asia/Bishkek", 252 | "Asia/Brunei", 253 | "Asia/Calcutta", 254 | "Asia/Chita", 255 | "Asia/Choibalsan", 256 | "Asia/Chongqing", 257 | "Asia/Chungking", 258 | "Asia/Colombo", 259 | "Asia/Dacca", 260 | "Asia/Damascus", 261 | "Asia/Dhaka", 262 | "Asia/Dili", 263 | "Asia/Dubai", 264 | "Asia/Dushanbe", 265 | "Asia/Famagusta", 266 | "Asia/Gaza", 267 | "Asia/Harbin", 268 | "Asia/Hebron", 269 | "Asia/Ho_Chi_Minh", 270 | "Asia/Hong_Kong", 271 | "Asia/Hovd", 272 | "Asia/Irkutsk", 273 | "Asia/Istanbul", 274 | "Asia/Jakarta", 275 | "Asia/Jayapura", 276 | "Asia/Jerusalem", 277 | "Asia/Kabul", 278 | "Asia/Kamchatka", 279 | "Asia/Karachi", 280 | "Asia/Kashgar", 281 | "Asia/Kathmandu", 282 | "Asia/Katmandu", 283 | "Asia/Khandyga", 284 | "Asia/Kolkata", 285 | "Asia/Krasnoyarsk", 286 | "Asia/Kuala_Lumpur", 287 | "Asia/Kuching", 288 | "Asia/Kuwait", 289 | "Asia/Macao", 290 | "Asia/Macau", 291 | "Asia/Magadan", 292 | "Asia/Makassar", 293 | "Asia/Manila", 294 | "Asia/Muscat", 295 | "Asia/Nicosia", 296 | "Asia/Novokuznetsk", 297 | "Asia/Novosibirsk", 298 | "Asia/Omsk", 299 | "Asia/Oral", 300 | "Asia/Phnom_Penh", 301 | "Asia/Pontianak", 302 | "Asia/Pyongyang", 303 | "Asia/Qatar", 304 | "Asia/Qostanay", 305 | "Asia/Qyzylorda", 306 | "Asia/Rangoon", 307 | "Asia/Riyadh", 308 | "Asia/Saigon", 309 | "Asia/Sakhalin", 310 | "Asia/Samarkand", 311 | "Asia/Seoul", 312 | "Asia/Shanghai", 313 | "Asia/Singapore", 314 | "Asia/Srednekolymsk", 315 | "Asia/Taipei", 316 | "Asia/Tashkent", 317 | "Asia/Tbilisi", 318 | "Asia/Tehran", 319 | "Asia/Tel_Aviv", 320 | "Asia/Thimbu", 321 | "Asia/Thimphu", 322 | "Asia/Tokyo", 323 | "Asia/Tomsk", 324 | "Asia/Ujung_Pandang", 325 | "Asia/Ulaanbaatar", 326 | "Asia/Ulan_Bator", 327 | "Asia/Urumqi", 328 | "Asia/Ust-Nera", 329 | "Asia/Vientiane", 330 | "Asia/Vladivostok", 331 | "Asia/Yakutsk", 332 | "Asia/Yangon", 333 | "Asia/Yekaterinburg", 334 | "Asia/Yerevan", 335 | "Atlantic/Azores", 336 | "Atlantic/Bermuda", 337 | "Atlantic/Canary", 338 | "Atlantic/Cape_Verde", 339 | "Atlantic/Faeroe", 340 | "Atlantic/Faroe", 341 | "Atlantic/Jan_Mayen", 342 | "Atlantic/Madeira", 343 | "Atlantic/Reykjavik", 344 | "Atlantic/South_Georgia", 345 | "Atlantic/St_Helena", 346 | "Atlantic/Stanley", 347 | "Australia/ACT", 348 | "Australia/Adelaide", 349 | "Australia/Brisbane", 350 | "Australia/Broken_Hill", 351 | "Australia/Canberra", 352 | "Australia/Currie", 353 | "Australia/Darwin", 354 | "Australia/Eucla", 355 | "Australia/Hobart", 356 | "Australia/LHI", 357 | "Australia/Lindeman", 358 | "Australia/Lord_Howe", 359 | "Australia/Melbourne", 360 | "Australia/NSW", 361 | "Australia/North", 362 | "Australia/Perth", 363 | "Australia/Queensland", 364 | "Australia/South", 365 | "Australia/Sydney", 366 | "Australia/Tasmania", 367 | "Australia/Victoria", 368 | "Australia/West", 369 | "Australia/Yancowinna", 370 | "Brazil/Acre", 371 | "Brazil/DeNoronha", 372 | "Brazil/East", 373 | "Brazil/West", 374 | "CEST", 375 | "CET", 376 | "CST6CDT", 377 | "Canada/Atlantic", 378 | "Canada/Central", 379 | "Canada/Eastern", 380 | "Canada/Mountain", 381 | "Canada/Newfoundland", 382 | "Canada/Pacific", 383 | "Canada/Saskatchewan", 384 | "Canada/Yukon", 385 | "Chile/Continental", 386 | "Chile/EasterIsland", 387 | "Cuba", 388 | "EDT", 389 | "EET", 390 | "EST", 391 | "EST5EDT", 392 | "Egypt", 393 | "Eire", 394 | "Etc/GMT", 395 | "Etc/GMT+0", 396 | "Etc/GMT+1", 397 | "Etc/GMT+10", 398 | "Etc/GMT+11", 399 | "Etc/GMT+12", 400 | "Etc/GMT+2", 401 | "Etc/GMT+3", 402 | "Etc/GMT+4", 403 | "Etc/GMT+5", 404 | "Etc/GMT+6", 405 | "Etc/GMT+7", 406 | "Etc/GMT+8", 407 | "Etc/GMT+9", 408 | "Etc/GMT-0", 409 | "Etc/GMT-1", 410 | "Etc/GMT-10", 411 | "Etc/GMT-11", 412 | "Etc/GMT-12", 413 | "Etc/GMT-13", 414 | "Etc/GMT-14", 415 | "Etc/GMT-2", 416 | "Etc/GMT-3", 417 | "Etc/GMT-4", 418 | "Etc/GMT-5", 419 | "Etc/GMT-6", 420 | "Etc/GMT-7", 421 | "Etc/GMT-8", 422 | "Etc/GMT-9", 423 | "Etc/GMT0", 424 | "Etc/Greenwich", 425 | "Etc/UCT", 426 | "Etc/UTC", 427 | "Etc/Universal", 428 | "Etc/Zulu", 429 | "Europe/Amsterdam", 430 | "Europe/Andorra", 431 | "Europe/Astrakhan", 432 | "Europe/Athens", 433 | "Europe/Belfast", 434 | "Europe/Belgrade", 435 | "Europe/Berlin", 436 | "Europe/Bratislava", 437 | "Europe/Brussels", 438 | "Europe/Bucharest", 439 | "Europe/Budapest", 440 | "Europe/Busingen", 441 | "Europe/Chisinau", 442 | "Europe/Copenhagen", 443 | "Europe/Dublin", 444 | "Europe/Gibraltar", 445 | "Europe/Guernsey", 446 | "Europe/Helsinki", 447 | "Europe/Isle_of_Man", 448 | "Europe/Istanbul", 449 | "Europe/Jersey", 450 | "Europe/Kaliningrad", 451 | "Europe/Kiev", 452 | "Europe/Kirov", 453 | "Europe/Lisbon", 454 | "Europe/Ljubljana", 455 | "Europe/London", 456 | "Europe/Luxembourg", 457 | "Europe/Madrid", 458 | "Europe/Malta", 459 | "Europe/Mariehamn", 460 | "Europe/Minsk", 461 | "Europe/Monaco", 462 | "Europe/Moscow", 463 | "Europe/Nicosia", 464 | "Europe/Oslo", 465 | "Europe/Paris", 466 | "Europe/Podgorica", 467 | "Europe/Prague", 468 | "Europe/Riga", 469 | "Europe/Rome", 470 | "Europe/Samara", 471 | "Europe/San_Marino", 472 | "Europe/Sarajevo", 473 | "Europe/Saratov", 474 | "Europe/Simferopol", 475 | "Europe/Skopje", 476 | "Europe/Sofia", 477 | "Europe/Stockholm", 478 | "Europe/Tallinn", 479 | "Europe/Tirane", 480 | "Europe/Tiraspol", 481 | "Europe/Ulyanovsk", 482 | "Europe/Uzhgorod", 483 | "Europe/Vaduz", 484 | "Europe/Vatican", 485 | "Europe/Vienna", 486 | "Europe/Vilnius", 487 | "Europe/Volgograd", 488 | "Europe/Warsaw", 489 | "Europe/Zagreb", 490 | "Europe/Zaporozhye", 491 | "Europe/Zurich", 492 | "GB", 493 | "GB-Eire", 494 | "GMT", 495 | "GMT+0", 496 | "GMT-0", 497 | "GMT0", 498 | "Greenwich", 499 | "HST", 500 | "Hongkong", 501 | "Iceland", 502 | "Indian/Antananarivo", 503 | "Indian/Chagos", 504 | "Indian/Christmas", 505 | "Indian/Cocos", 506 | "Indian/Comoro", 507 | "Indian/Kerguelen", 508 | "Indian/Mahe", 509 | "Indian/Maldives", 510 | "Indian/Mauritius", 511 | "Indian/Mayotte", 512 | "Indian/Reunion", 513 | "Iran", 514 | "Israel", 515 | "Jamaica", 516 | "Japan", 517 | "Kwajalein", 518 | "Libya", 519 | "MET", 520 | "MST", 521 | "MST7MDT", 522 | "Mexico/BajaNorte", 523 | "Mexico/BajaSur", 524 | "Mexico/General", 525 | "NZ", 526 | "NZ-CHAT", 527 | "Navajo", 528 | "PRC", 529 | "PST", 530 | "PDT", 531 | "PST8PDT", 532 | "Pacific/Apia", 533 | "Pacific/Auckland", 534 | "Pacific/Bougainville", 535 | "Pacific/Chatham", 536 | "Pacific/Chuuk", 537 | "Pacific/Easter", 538 | "Pacific/Efate", 539 | "Pacific/Enderbury", 540 | "Pacific/Fakaofo", 541 | "Pacific/Fiji", 542 | "Pacific/Funafuti", 543 | "Pacific/Galapagos", 544 | "Pacific/Gambier", 545 | "Pacific/Guadalcanal", 546 | "Pacific/Guam", 547 | "Pacific/Honolulu", 548 | "Pacific/Johnston", 549 | "Pacific/Kiritimati", 550 | "Pacific/Kosrae", 551 | "Pacific/Kwajalein", 552 | "Pacific/Majuro", 553 | "Pacific/Marquesas", 554 | "Pacific/Midway", 555 | "Pacific/Nauru", 556 | "Pacific/Niue", 557 | "Pacific/Norfolk", 558 | "Pacific/Noumea", 559 | "Pacific/Pago_Pago", 560 | "Pacific/Palau", 561 | "Pacific/Pitcairn", 562 | "Pacific/Pohnpei", 563 | "Pacific/Ponape", 564 | "Pacific/Port_Moresby", 565 | "Pacific/Rarotonga", 566 | "Pacific/Saipan", 567 | "Pacific/Samoa", 568 | "Pacific/Tahiti", 569 | "Pacific/Tarawa", 570 | "Pacific/Tongatapu", 571 | "Pacific/Truk", 572 | "Pacific/Wake", 573 | "Pacific/Wallis", 574 | "Pacific/Yap", 575 | "Poland", 576 | "Portugal", 577 | "ROC", 578 | "ROK", 579 | "Singapore", 580 | "Turkey", 581 | "UCT", 582 | "US/Alaska", 583 | "US/Aleutian", 584 | "US/Arizona", 585 | "US/Central", 586 | "US/East-Indiana", 587 | "US/Eastern", 588 | "US/Hawaii", 589 | "US/Indiana-Starke", 590 | "US/Michigan", 591 | "US/Mountain", 592 | "US/Pacific", 593 | "US/Samoa", 594 | "UTC", 595 | "Universal", 596 | "W-SU", 597 | "WET", 598 | "Zulu" 599 | ] 600 | -------------------------------------------------------------------------------- /src/lib/util/data.js: -------------------------------------------------------------------------------- 1 | export function flatten(object) { 2 | // specifically instance of Error, not API responses which may have other properties 3 | object = object instanceof Error ? { message: object.message } : object; 4 | const entries = []; 5 | for (let [k, v] of Object.entries(object)) { 6 | if (typeof v === 'string') { 7 | try { 8 | let j = JSON.parse(v); 9 | if (typeof j === 'object') v = flatten(j); 10 | else v = String(j); 11 | } catch { 12 | /* empty */ 13 | } 14 | } else if (typeof v === 'object') { 15 | v = flatten(v); 16 | } else { 17 | v = v.toString(); 18 | } 19 | entries.push([k, v]); 20 | } 21 | return entries; 22 | } 23 | -------------------------------------------------------------------------------- /src/routes/(default)/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/routes/(default)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
20 | 28 | {#if $navigating || !mounted} 29 |
30 | 31 |
32 | {:else} 33 | {@render children?.()} 34 | {/if} 35 |
36 | -------------------------------------------------------------------------------- /src/routes/(default)/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | // import { importJSON } from '@eartharoid/vite-plugin-i18n'; // doesn't work? 3 | import { importJSON } from '$lib/i18n'; 4 | 5 | /** @type {import('./$types').PageLoad} */ 6 | export async function load({ parent, fetch }) { 7 | // TODO: remove this when the portal section is more complete 8 | redirect(302, '/settings'); 9 | 10 | const { locale } = await parent(); 11 | const guilds = await (await fetch(`/api/guilds`)).json(); 12 | if (guilds.length === 0) { 13 | redirect(302, '/settings'); 14 | } else if (guilds.length === 1) { 15 | redirect(302, `/${guilds[0].id}`); 16 | } 17 | return { 18 | translations: importJSON( 19 | await import(`../../lib/locales/${locale}/_common.json`), 20 | await import(`../../lib/locales/${locale}/misc.json`) 21 | ), 22 | guilds 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/routes/(default)/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | {t('select_server_title', { username: client.username })} 12 | 13 | 14 | 15 |
18 |
19 |

20 | {t('select_server')} 21 |

22 |
23 | {#each guilds as guild} 24 | {@const slug = BigInt(guild.id).toString(36)} 25 | 26 |
29 |
30 | 35 |

{guild.name}

36 |
37 |
38 |
39 | {/each} 40 |
41 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /src/routes/(default)/[guild]/+layout.js: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | import Big from 'big-integer'; 3 | 4 | /** @type {import('./$types').PageLoad} */ 5 | export async function load({ fetch, params }) { 6 | if (params.guild.split('.')[0] === 'favicon') error(404, 'Not Found'); 7 | const guildId = new Big(params.guild, 36); 8 | const response = await fetch(`/api/guilds/${guildId}`); 9 | const body = await response.json(); 10 | if (!response.ok) error(response.status, JSON.stringify(body)); 11 | return { 12 | guild: body 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/(default)/[guild]/+layout.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | {@render children?.()} 14 |
15 | -------------------------------------------------------------------------------- /src/routes/(default)/[guild]/+page.js: -------------------------------------------------------------------------------- 1 | import { importJSON } from '$lib/i18n'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ parent }) { 5 | const { locale } = await parent(); 6 | return { 7 | translations: importJSON(await import(`../../../lib/locales/${locale}/_common.json`)) 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/(default)/[guild]/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {t('common:title', { guild: guild.name, client: client.username })} 13 | 14 | 15 |
16 | {#if guild.privilegeLevel > 0} 17 | 18 |
19 |
20 |
21 | 22 | 23 | 27 |
28 | 29 | {t('common:staff_dashboard')} 30 |
31 |
32 | {#if guild.privilegeLevel >= 2} 33 | 34 | 38 |
39 | 40 | {t('common:settings_panel')} 41 |
42 |
43 | {/if} 44 |
45 |
46 |
47 | {/if} 48 |
todo
49 |
50 | -------------------------------------------------------------------------------- /src/routes/(default)/[guild]/feedback/+page.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/src/routes/(default)/[guild]/feedback/+page.svelte -------------------------------------------------------------------------------- /src/routes/(default)/[guild]/staff/+page.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/src/routes/(default)/[guild]/staff/+page.svelte -------------------------------------------------------------------------------- /src/routes/(default)/[guild]/tickets/+page.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/src/routes/(default)/[guild]/tickets/+page.svelte -------------------------------------------------------------------------------- /src/routes/(default)/invite/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ url }) { 5 | redirect(307, `/auth/login?invite&guild=${url.searchParams.get('guild') || ''}`); 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/routes/(default)/login/+page.js: -------------------------------------------------------------------------------- 1 | import { importJSON } from '$lib/i18n'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ parent, url }) { 5 | const { locale } = await parent(); 6 | return { 7 | translations: importJSON( 8 | await import(`../../../lib/locales/${locale}/_common.json`), 9 | await import(`../../../lib/locales/${locale}/misc.json`) 10 | ), 11 | query: url.search 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/(default)/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {t('login_title', { username: client.username })} 15 | 16 | 17 | 18 |
22 |
25 |
26 |
27 | 28 |
29 |
30 | 31 |

{client.username}

32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {t('please_login')} 41 |
42 | 46 | 47 | 48 | {t('continue_with_discord')} 49 | 50 | 51 |
52 |
55 |
56 | 57 |
58 |
59 | 60 | {t('common:language')} 61 |
62 |
63 | 64 |
65 |
66 | 67 | {t('common:theme')} 68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /src/routes/(default)/view/[ticket]/+page.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/src/routes/(default)/view/[ticket]/+page.svelte -------------------------------------------------------------------------------- /src/routes/+layout.server.js: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit'; 2 | import Negotiator from 'negotiator'; 3 | import { getSupportedLocales } from '$lib/i18n'; 4 | import ms from 'ms'; 5 | 6 | /** @type {import('./$types').LayoutServerLoad} */ 7 | export async function load({ cookies, fetch, request, url }) { 8 | if (url.pathname === '/invite') { 9 | redirect(307, `/auth/login?invite&guild=${url.searchParams.get('guild') || ''}`) 10 | } 11 | const response = await fetch(`/api/users/@me`); 12 | const isJSON = response.headers.get('Content-Type')?.includes('json'); 13 | const body = isJSON ? await response.json() : await response.text(); 14 | if (url.pathname !== '/login') { 15 | if (response.status === 401) { 16 | let qs = `r=${encodeURIComponent(url.pathname + url.search)}`; 17 | if (url.pathname.startsWith('/settings')) { 18 | qs += '&role=admin'; 19 | } 20 | redirect(307, `/login?${qs}`); 21 | } else if (!response.ok) { 22 | error(response.status, isJSON ? JSON.stringify(body) : body); 23 | } 24 | } 25 | let locale = cookies.get('locale'); 26 | if (!locale) { 27 | const supportedLocales = getSupportedLocales(); 28 | if (supportedLocales.includes(body.locale)) { 29 | locale = body.locale; 30 | } else { 31 | const negotiator = new Negotiator(request); 32 | locale = negotiator.language(supportedLocales); 33 | } 34 | cookies.set('locale', locale, { 35 | maxAge: ms('1y') / 1000, 36 | path: '/', 37 | sameSite: 'lax', 38 | secure: false, 39 | httpOnly: false 40 | }); 41 | } 42 | return { 43 | client: await (await fetch(`/api/client`, { credentials: 'include' })).json(), 44 | locale, 45 | theme: cookies.get('theme'), 46 | user: body 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
32 | {@render children?.()} 33 |
34 | -------------------------------------------------------------------------------- /src/routes/settings/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/routes/settings/+layout.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | Discord Tickets 52 | 53 | 54 | 55 |
56 | 57 | {#snippet backdrop({ close })} 58 |
close()} 62 | onkeypress={() => close()} 63 | >
64 | {/snippet} 65 | {#snippet loading()} 66 |
67 | 68 |
69 | {/snippet} 70 |
71 | {#if mounted && client.public && !cookies.dismissedCookies} 72 |
75 |

Cookies are being used to store credentials and preferences.

76 |

77 | 83 |

84 |
85 | {/if} 86 |
87 | {#if $navigating || !mounted} 88 |
89 | 90 |
91 | {:else} 92 |
93 |
94 | 95 | {@render children?.()} 96 | 163 |
164 |
165 | {/if} 166 |
167 |
168 | -------------------------------------------------------------------------------- /src/routes/settings/+page.js: -------------------------------------------------------------------------------- 1 | /** @type {import('./$types').PageLoad} */ 2 | export async function load({ fetch }) { 3 | const fetchOptions = { credentials: 'include' }; 4 | return { 5 | guilds: await (await fetch(`/api/admin/guilds`, fetchOptions)).json() 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/settings/+page.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 |
24 |
25 |
26 | {#if good.length === 0} 27 |
28 |

Add your bot to a guild to get started

29 |
30 | {:else} 31 |
32 |

Manage your guilds

33 |
34 | {#each good as guild} 35 | 36 | 42 | 43 | {/each} 44 | {#if bad.length > 0} 45 |
46 | {/if} 47 | {/if} 48 |
49 | 50 | {#if good.length > 0} 51 |
52 |

Add your bot to more guilds

53 |
54 | {/if} 55 | 56 |
57 | {#each bad as guild} 58 | 59 | 65 | 66 | {/each} 67 | 68 | 75 | 76 |
77 |
78 |
79 |
80 |
81 |
84 | 85 | 86 | {client.username}#{client.discriminator} 89 | 90 |
91 |
92 |
93 |
Activated users
94 |

{formatter.format(client.stats.activatedUsers)}

95 |
96 |
97 |
Archived messages
98 |

{formatter.format(client.stats.archivedMessages)}

99 |
100 |
101 |
Resolution time
102 |

{client.stats.avgResolutionTime}

103 |
104 |
105 |
Response time
106 |

{client.stats.avgResponseTime}

107 |
108 |
109 |
Categories
110 |

{formatter.format(client.stats.categories)}

111 |
112 |
113 |
Guilds
114 |

{formatter.format(client.stats.guilds)}

115 |
116 | 126 |
127 |
Members (avg)
128 |

129 | {formatter.format(client.stats.members)} 130 | ({formatter.format(Math.floor(client.stats.members / client.stats.guilds))}) 131 |

132 |
133 |
134 |
Tags
135 |

{formatter.format(client.stats.tags)}

136 |
137 |
138 |
Tickets
139 |

{formatter.format(client.stats.tickets)}

140 |
141 |
142 |
143 |
144 |
145 | 146 | 173 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/+layout.js: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ fetch, params, url }) { 5 | const response = await fetch(`/api/admin/guilds/${params.guild}`); 6 | const isJSON = response.headers.get('Content-Type')?.includes('json'); 7 | const body = isJSON ? await response.json() : await response.text(); 8 | if (response.status === 401 && body.elevate) { 9 | redirect(307, `/auth/login?r=${encodeURIComponent(url.pathname + url.search)}&role=${body.elevate}`); 10 | } else if (!response.ok) { 11 | error(response.status, isJSON ? JSON.stringify(body) : body); 12 | } else { 13 | return { guild: body }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/+layout.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | {@render children?.()} 19 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/+page.js: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ fetch, params, url }) { 5 | const fetchOptions = { credentials: 'include' }; 6 | const response = await fetch(`/api/admin/guilds/${params.guild}`, fetchOptions); 7 | const isJSON = response.headers.get('Content-Type')?.includes('json'); 8 | const body = isJSON ? await response.json() : await response.text(); 9 | if (response.status === 401 && body.elevate) { 10 | redirect(307, `/auth/login?r=${encodeURIComponent(url.pathname + url.search)}&role=${body.elevate}`); 11 | } else if (!response.ok) { 12 | error(response.status, isJSON ? JSON.stringify(body) : body); 13 | } else { 14 | return { 15 | guild: body, 16 | problems: await ( 17 | await fetch(`/api/admin/guilds/${params.guild}/problems`, fetchOptions) 18 | ).json() 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/+page@settings.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#snippet botPublic()} 24 | WARNING: 25 | This bot is public; anyone can add it to their servers. Is this a mistake? Learn more at 26 | https://lnk.earth/dt-warn-pub. 31 | {/snippet} 32 | 33 | {#snippet logChannelMissingPermission(p)} 34 | Please give the bot {p.permission} permission in the log channel. 35 | {/snippet} 36 | 37 |
38 |
39 | {#each problems as p} 40 |
41 |
44 |
45 | 46 |
47 | {@render problemSnippets[p.id]?.(p)} 48 |
49 |
50 |
51 |
52 | {/each} 53 | {#if guild.stats.categories.length === 0} 54 | 67 | {/if} 68 |
69 | 73 | 74 |

General

75 |
76 | 80 | 81 |

Categories

82 |
83 | 87 | 88 |

Panels

89 |
90 | 94 | 95 |

Feedback

96 |
97 | 101 | 102 |

Tags

103 |
104 | 111 |
112 |
113 |
114 |
115 |
118 | 119 |

120 | 121 | {guild.name} 122 | 123 |
124 | 125 | 126 | Added on 127 | {createdAt} 128 | 129 |

130 |
131 |
132 |
133 |
Resolution time
134 |

{guild.stats.avgResolutionTime}

135 |
136 |
137 |
Response time
138 |

{guild.stats.avgResponseTime}

139 |
140 |
141 |
Categories
142 |

{guild.stats.categories.length}

143 |
144 |
145 |
Tags
146 |

{guild.stats.tags}

147 |
148 |
149 |
Tickets
150 |

{formatter.format(guild.stats.tickets)}

151 |
152 |
153 |
Most used category
154 |

155 | {guild.stats.categories.sort((a, b) => b.tickets - a.tickets)[0]?.name ?? 'None'} 156 |

157 |
158 |
159 |
160 |
161 |
162 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/categories/+page.js: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ fetch, params }) { 5 | const response = await fetch(`/api/admin/guilds/${params.guild}/categories`); 6 | const isJSON = response.headers.get('Content-Type')?.includes('json'); 7 | const body = isJSON ? await response.json() : await response.text(); 8 | if (!response.ok) { 9 | error(response.status, isJSON ? JSON.stringify(body) : body); 10 | } else { 11 | return { categories: body }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/categories/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |

Categories

11 | 68 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/categories/[category]/+page.js: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ fetch, params }) { 5 | const fetchOptions = { credentials: 'include' }; 6 | let body; 7 | if (params.category === 'new') { 8 | body = { 9 | channelName: '', 10 | claiming: false, 11 | description: '', 12 | discordCategory: 'new', 13 | enableFeedback: false, 14 | emoji: '', 15 | image: '', 16 | memberLimit: 1, 17 | name: '', 18 | openingMessage: '', 19 | pingRoles: [], 20 | questions: [], 21 | ratelimit: null, 22 | requiredRoles: [], 23 | requireTopic: false, 24 | staffRoles: [], 25 | totalLimit: 50 26 | }; 27 | } else { 28 | const response = await fetch( 29 | `/api/admin/guilds/${params.guild}/categories/${params.category}`, 30 | fetchOptions 31 | ); 32 | const isJSON = response.headers.get('Content-Type')?.includes('json'); 33 | body = isJSON ? await response.json() : await response.text(); 34 | if (!response.ok) { 35 | error(response.status, isJSON ? JSON.stringify(body) : body); 36 | } 37 | } 38 | 39 | let url = `/api/admin/guilds/${params.guild}/categories`; 40 | if (params.category !== 'new') url += `/${params.category}`; 41 | 42 | return { 43 | url, 44 | category: body, 45 | channels: await ( 46 | await fetch(`/api/admin/guilds/${params.guild}/data?query=channels.cache`, fetchOptions) 47 | ).json(), 48 | roles: await ( 49 | await fetch(`/api/admin/guilds/${params.guild}/data?query=roles.cache`, fetchOptions) 50 | ).json(), 51 | settings: await (await fetch(`/api/admin/guilds/${params.guild}/settings`, fetchOptions)).json() 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/categories/[category]/+page.svelte: -------------------------------------------------------------------------------- 1 | 171 | 172 |
173 |

174 | 175 | Read the documentation 179 | to avoid problems. 180 |

181 |
182 |

Categories

183 |

184 | {emoji.get(category.emoji) ?? ''} 185 | {category.name || 'New category'} 186 |

187 |
188 | {#if error} 189 | 190 | {/if} 191 |
submit())} onchange={() => (modified = true)} class="my-4"> 192 |
193 |
194 |
195 | 204 |
205 |
206 | 223 | {#if category.channelName} 224 |

Preview

225 |
228 | 229 | 230 | {@html marked 231 | .parse(category.channelName.replace(/\n/g, '\n\n')) 232 | .replace(/{+\s?num(ber)?\s?}+/gi, 1) 233 | .replace(/{+\s?(nick|display)(name)?\s?}+/gi, getContext('user').username) 234 | .replace(/{+\s?(user)?name\s?}+/gi, getContext('user').username)} 235 | 236 |
237 | {/if} 238 |
239 |
240 | 254 |
255 |
256 | 264 |
265 |
266 | 280 |
281 |
282 | 302 |
303 |
304 | 314 |
315 |
316 | 330 |
331 |
332 | 340 |
341 |
342 | 356 |
357 |
358 | 373 | {#key category.pingRoles} 374 | {#key category.requireTopic} 375 | {#if category.openingMessage} 376 |

Preview

377 | 382 | 393 | {#if category.pingRoles?.length > 0} 394 | {#each category.pingRoles as id, index} 395 | {@const role = getRole(id)} 396 | {#if role} 397 | {#if index > 0} 398 | {' '} 399 | {/if} 400 | 401 | {role?.name} 402 | 403 | {/if} 404 | {/each} 405 | ,
406 | {/if} 407 | {data.user.username} 408 | has created a new ticket 409 | 416 | 417 | {@html marked 418 | .parse(category.openingMessage) 419 | .replace( 420 | /{+\s?(user)?name\s?}+/gi, 421 | `${data.user.username}` 422 | ) 423 | .replace(/{+\s?avgResponseTime\s?}+/gi, data.guild.stats.avgResponseTime) 424 | .replace( 425 | /{+\s?avgResolutionTime\s?}+/gi, 426 | data.guild.stats.avgResolutionTime 427 | )} 428 | 429 | {#if category.requireTopic} 430 | 431 | 432 | This is a pretty good preview 433 | 434 | 435 | {/if} 436 | {#if data.settings.footer} 437 | 438 | {data.settings.footer} 439 | 440 | {/if} 441 | 442 | 443 | 444 | {#if category.requireTopic || qS.questions.length > 0} 445 | ✏️ Edit 446 | {/if} 447 | {#if category.claiming && data.settings.claimButton} 448 | 🙌 Claim 449 | {/if} 450 | {#if data.settings.closeButton} 451 | ✖️ Close 452 | {/if} 453 | 454 | 455 |
456 |
457 | {/if} 458 | {/key} 459 | {/key} 460 |
461 |
462 | 482 |
483 |
484 | 503 |
504 |
505 | 525 |
526 |
527 | 542 |
543 |
544 | 566 |
567 |
568 | 582 |
583 |
584 |
585 |
586 |
587 |
588 |

Questions

589 |

{qS.questions.length}/5

590 |
591 | {#if qS.questions.length > 0} 592 |
593 | 616 |
617 | {/if} 618 |
619 | 620 |
621 | {#if qS.questions.length < 5} 622 |
623 | 646 |
647 | {/if} 648 |
649 |
650 |
651 | {#if category.id} 652 | 665 | {/if} 666 | 676 |
677 |
678 |
679 |
680 |
681 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/feedback/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |

Feedback

7 |
8 |
9 | Reviews illustration 16 |
17 | 26 |
27 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/general/+page.js: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | /** @type {import('./$types').PageLoad} */ 3 | export async function load({ fetch, params }) { 4 | const response = await fetch(`/api/admin/guilds/${params.guild}/settings`); 5 | const isJSON = response.headers.get('Content-Type')?.includes('json'); 6 | const body = isJSON ? await response.json() : await response.text(); 7 | if (!response.ok) { 8 | error(response.status, isJSON ? JSON.stringify(body) : body); 9 | } else { 10 | return { 11 | settings: body, 12 | channels: await ( 13 | await fetch(`/api/admin/guilds/${params.guild}/data?query=channels.cache`) 14 | ).json(), 15 | locales: await (await fetch(`/api/locales`)).json(), 16 | roles: await (await fetch(`/api/admin/guilds/${params.guild}/data?query=roles.cache`)).json() 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/general/+page.svelte: -------------------------------------------------------------------------------- 1 | 97 | 98 |

General settings

99 |
100 | {#if error} 101 | 102 | {/if} 103 |
104 |

Warning

105 |

106 | This page is made to be "just about functional". 107 | Read the documentation 111 | to avoid breaking something. 112 |

113 |
114 |
submit())} onchange={() => (modified = true)}> 115 |
116 |
117 | 125 |
126 |
127 | 154 |
155 |
156 | 170 |
171 |
172 | 192 |
193 |
194 |
195 | Buttons 196 | 200 |
201 |
202 | 216 |
217 |
218 | 232 |
233 |
234 |
235 |
236 |
237 | 245 |
246 |
247 | 255 |
256 |
257 | 272 |
273 |
274 | 291 |
292 |
293 | 301 |
302 |
303 | 311 |
312 |
313 | 321 |
322 |
323 |
324 |
325 | Working hours 326 | 330 |

(expanded.workingHours = !expanded.workingHours)} 333 | > 334 | 339 | Click to {expanded.workingHours ? 'collapse' : 'expand'} 340 |

341 |
342 | 343 | {#if expanded.workingHours} 344 |
345 |
346 | 366 | {#each days as day, index} 367 | 383 | {/each} 384 |
385 |
386 | {/if} 387 |
388 |
389 |
390 | 400 |
401 |
402 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/panels/+page.js: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ fetch, params }) { 5 | const fetchOptions = { credentials: 'include' }; 6 | const response = await fetch(`/api/admin/guilds/${params.guild}/categories`, fetchOptions); 7 | const isJSON = response.headers.get('Content-Type')?.includes('json'); 8 | const body = isJSON ? await response.json() : await response.text(); 9 | if (!response.ok) { 10 | error(response.status, isJSON ? JSON.stringify(body) : body); 11 | } else { 12 | return { 13 | categories: body, 14 | channels: await ( 15 | await fetch(`/api/admin/guilds/${params.guild}/data?query=channels.cache`, fetchOptions) 16 | ).json() 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/panels/+page.svelte: -------------------------------------------------------------------------------- 1 | 79 | 80 |

Create a panel

81 |
82 | {#if error} 83 | 84 | {/if} 85 | 86 |
87 |
88 | {#if panel.channel !== 'new' && panel.type === 'MESSAGE'} 89 |

90 | 91 | Make sure members can read and send messages in 92 | #{getChannelName(panel.channel)}. 93 |

94 | {:else if panel.channel !== 'new' && panel.type !== 'MESSAGE'} 95 |

96 | 97 | Make sure members can read but not send messages in 98 | #{getChannelName(panel.channel)}. 99 |

100 | {/if} 101 |
102 |
submit())} class="my-4 text-lg"> 103 |
104 |
105 | 126 |
127 |
128 | 146 |
147 |
148 | 173 |
174 |
175 | 183 |
184 |
185 | 193 |
194 |
195 | 203 |
204 |
205 | 214 |
215 |
216 | 226 |
227 |
228 |
229 |
230 |
231 |
232 | 233 |
234 | Looking to edit or remove a panel? Just delete the message or channel in Discord. 235 |
236 |
237 |
238 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/tags/+page.js: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ fetch, params }) { 5 | const response = await fetch(`/api/admin/guilds/${params.guild}/tags`); 6 | const isJSON = response.headers.get('Content-Type')?.includes('json'); 7 | const body = isJSON ? await response.json() : await response.text(); 8 | if (!response.ok) { 9 | error(response.status, isJSON ? JSON.stringify(body) : body); 10 | } else { 11 | return { 12 | tags: body 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/settings/[guild]/tags/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | 153 | 154 |
155 |

156 | 157 | Read the documentation 160 | to avoid problems. 161 |

162 |
163 |

Tags

164 | {#if error} 165 | 166 | {/if} 167 |
168 |
169 |
170 |
171 | filter(event.target.value)} 177 | /> 178 |
179 | {#each shown as tag, i} 180 |
181 | {tag.name} 182 |

(expanded = expanded === tag.id ? null : tag.id)} 185 | > 186 | 191 | Click to {expanded === tag.id ? 'collapse' : 'expand'} 192 |

193 | {#if expanded === tag.id} 194 |
195 |
save(tag))} id={tag.id} name={tag.name}> 196 | 197 | 198 |
199 | 212 | 224 |
225 |
226 | {/if} 227 |
228 | {/each} 229 |
230 |
231 |
232 |
233 |

Create a tag

234 |
create())} class="my-4 text-lg"> 235 |
236 | 237 | 247 |
248 |
249 |
250 |
251 |
252 | 253 | 254 | {#snippet children({ data: toasted })} 255 | 256 | {/snippet} 257 | 258 | -------------------------------------------------------------------------------- /static/assets/topgg-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/static/assets/topgg-dark.webp -------------------------------------------------------------------------------- /static/assets/topgg-light.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/static/assets/topgg-light.webp -------------------------------------------------------------------------------- /static/assets/undraw_reviews.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/assets/wordmark-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/static/assets/wordmark-dark.png -------------------------------------------------------------------------------- /static/assets/wordmark-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/static/assets/wordmark-light.png -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord-tickets/portal/6f8ac9b91b53c56c11db88796a9b1a6ae948cebb/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { sveltePreprocess } from 'svelte-preprocess'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | kit: { 7 | adapter: adapter({ out: 'build' }), 8 | alias: { 9 | $components: './src/components' 10 | } 11 | }, 12 | preprocess: [sveltePreprocess({ postcss: true })], 13 | trailingSlash: 'never' 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "4.0.0", 5 | "title": "Discord Tickets API", 6 | "description": "This is the schema for the API that you can use to interact with your ticket bot. It is used by the Archives Portal and the Settings Panel websites.\nIf you are using a managed ticket bot your API will be available at `https://my.discordtickets.app:{port}`. Create a ticket if you don't know what port your bot is on.\n# Error codes\nCheck the error `message` for more specific details.\n- `0x001`: Invalid type\n- `0x002`: String is too short\n- `0x003`: String is too long\n- `0x191`: Unauthorised\n- `0x193`: Forbidden\n- `0x194`: Not found\n- `0x1F4`: Internal server error" 7 | }, 8 | "servers": [ 9 | { 10 | "description": "[TEST] SwaggerHub API Auto Mocking", 11 | "url": "https://virtserver.swaggerhub.com/eartharoid/discord-tickets/4.0.0" 12 | }, 13 | { 14 | "url": "http://localhost:{port}/api", 15 | "description": "[DEV] Local development API server", 16 | "variables": { 17 | "port": { 18 | "default": "8080" 19 | } 20 | } 21 | }, 22 | { 23 | "url": "{host}/api", 24 | "description": "[PROD] Production API server", 25 | "variables": { 26 | "host": { 27 | "default": "http://localhost:8080" 28 | } 29 | } 30 | } 31 | ], 32 | "security": [ 33 | { 34 | "api_key": [] 35 | } 36 | ], 37 | "paths": { 38 | "/admin/guilds": { 39 | "get": { 40 | "tags": ["admin"], 41 | "summary": "List the guilds that the user has permission to manage", 42 | "description": "This route returns an array of guilds that the user can manage.", 43 | "responses": { 44 | "200": { 45 | "description": "OK", 46 | "content": { 47 | "application/json": { 48 | "schema": { 49 | "type": "array", 50 | "items": { 51 | "$ref": "#/components/schemas/Guild" 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | "default": { 58 | "$ref": "#/components/responses/default" 59 | } 60 | } 61 | } 62 | }, 63 | "/admin/guilds/{guild}": { 64 | "parameters": [ 65 | { 66 | "name": "guild", 67 | "in": "path", 68 | "description": "The guild to get or update the settings of", 69 | "required": true, 70 | "style": "simple", 71 | "schema": { 72 | "type": "string" 73 | } 74 | } 75 | ], 76 | "delete": { 77 | "tags": ["admin"], 78 | "summary": "Reset a guild's settings", 79 | "description": "This route resets the guild's settings.", 80 | "responses": { 81 | "200": { 82 | "description": "OK", 83 | "content": { 84 | "application/json": { 85 | "schema": { 86 | "$ref": "#/components/schemas/GuildSettings" 87 | } 88 | } 89 | } 90 | }, 91 | "default": { 92 | "$ref": "#/components/responses/default" 93 | } 94 | } 95 | }, 96 | "get": { 97 | "tags": ["admin"], 98 | "summary": "Get a guild's settings", 99 | "description": "This route returns the guild's current settings.", 100 | "responses": { 101 | "200": { 102 | "description": "OK", 103 | "content": { 104 | "application/json": { 105 | "schema": { 106 | "$ref": "#/components/schemas/GuildSettings" 107 | } 108 | } 109 | } 110 | }, 111 | "default": { 112 | "$ref": "#/components/responses/default" 113 | } 114 | } 115 | }, 116 | "put": { 117 | "tags": ["admin"], 118 | "summary": "Set a guild's settings", 119 | "description": "This route sets the guild's settings.", 120 | "requestBody": { 121 | "description": "The full GuildSettings object", 122 | "required": true, 123 | "content": { 124 | "application/json": { 125 | "schema": { 126 | "$ref": "#/components/schemas/GuildSettings" 127 | } 128 | } 129 | } 130 | }, 131 | "responses": { 132 | "200": { 133 | "description": "OK", 134 | "content": { 135 | "application/json": { 136 | "schema": { 137 | "$ref": "#/components/schemas/GuildSettings" 138 | } 139 | } 140 | } 141 | }, 142 | "default": { 143 | "$ref": "#/components/responses/default" 144 | } 145 | } 146 | } 147 | }, 148 | "/admin/guilds/{guild}/data": { 149 | "get": { 150 | "tags": ["admin"], 151 | "summary": "Get properties from the guild data", 152 | "description": "This route returns the requested property from the guild.", 153 | "parameters": [ 154 | { 155 | "name": "guild", 156 | "in": "path", 157 | "description": "The guild to reset the settings of", 158 | "required": true, 159 | "style": "simple", 160 | "schema": { 161 | "type": "string" 162 | } 163 | }, 164 | { 165 | "name": "query", 166 | "in": "query", 167 | "description": "The dot-notation property to get", 168 | "required": true, 169 | "style": "form", 170 | "schema": { 171 | "type": "string" 172 | } 173 | } 174 | ], 175 | "responses": { 176 | "200": { 177 | "description": "OK", 178 | "content": { 179 | "application/json": { 180 | "schema": { 181 | "type": "object" 182 | }, 183 | "example": 5 184 | } 185 | } 186 | }, 187 | "default": { 188 | "$ref": "#/components/responses/default" 189 | } 190 | } 191 | } 192 | }, 193 | "/admin/guilds/{guild}/categories": { 194 | "get": { 195 | "tags": ["admin"], 196 | "summary": "List the categories of a guild", 197 | "description": "This route returns an array of categories within a guild.", 198 | "parameters": [ 199 | { 200 | "name": "guild", 201 | "in": "path", 202 | "description": "The guild to list the categories of", 203 | "required": true, 204 | "style": "simple", 205 | "schema": { 206 | "type": "string" 207 | } 208 | } 209 | ], 210 | "responses": { 211 | "200": { 212 | "description": "OK", 213 | "content": { 214 | "application/json": { 215 | "schema": { 216 | "type": "array", 217 | "items": { 218 | "$ref": "#/components/schemas/Category" 219 | } 220 | } 221 | } 222 | } 223 | }, 224 | "default": { 225 | "$ref": "#/components/responses/default" 226 | } 227 | } 228 | }, 229 | "post": { 230 | "tags": ["admin"], 231 | "summary": "Create a new category", 232 | "description": "This route creates a new category within a guild.", 233 | "parameters": [ 234 | { 235 | "name": "guild", 236 | "in": "path", 237 | "description": "The guild to create a category in", 238 | "required": true, 239 | "style": "simple", 240 | "schema": { 241 | "type": "string" 242 | } 243 | } 244 | ], 245 | "requestBody": { 246 | "description": "The full CategorySettings object", 247 | "required": true, 248 | "content": { 249 | "application/json": { 250 | "schema": { 251 | "type": "object", 252 | "required": ["name", "openingMessage", "staffRoles"], 253 | "properties": { 254 | "discordCategory": { 255 | "type": "string" 256 | }, 257 | "name": { 258 | "type": "string" 259 | }, 260 | "openingMessage": { 261 | "type": "string" 262 | }, 263 | "staffRoles": { 264 | "type": "array", 265 | "items": { 266 | "type": "string" 267 | } 268 | } 269 | }, 270 | "example": { 271 | "discordCategory": null, 272 | "name": "Support", 273 | "openingMessage": "Thank you for creating a ticket. Please be patient, a member of the support team will reply when they become available. Whilst you wait, please provide as much information about your support query as possible. ", 274 | "roles": [451745787974778908, 513828182697312267] 275 | } 276 | } 277 | } 278 | } 279 | }, 280 | "responses": { 281 | "201": { 282 | "description": "Created", 283 | "content": { 284 | "application/json": { 285 | "schema": { 286 | "$ref": "#/components/schemas/CategorySettings" 287 | } 288 | } 289 | } 290 | }, 291 | "default": { 292 | "$ref": "#/components/responses/default" 293 | } 294 | } 295 | } 296 | }, 297 | "/admin/guilds/{guild}/categories/{category}": { 298 | "delete": { 299 | "tags": ["admin"], 300 | "summary": "Delete a category", 301 | "description": "This route deletes the category. Tickets within this category will **not** be deleted automatically.", 302 | "parameters": [ 303 | { 304 | "name": "guild", 305 | "in": "path", 306 | "description": "The guild the category belongs to", 307 | "required": true, 308 | "style": "simple", 309 | "schema": { 310 | "type": "string" 311 | } 312 | }, 313 | { 314 | "name": "category", 315 | "in": "path", 316 | "description": "The category to delete", 317 | "required": true, 318 | "style": "simple", 319 | "schema": { 320 | "type": "string" 321 | } 322 | } 323 | ], 324 | "responses": { 325 | "200": { 326 | "description": "OK", 327 | "content": { 328 | "application/json": { 329 | "schema": { 330 | "type": "object" 331 | } 332 | } 333 | } 334 | }, 335 | "default": { 336 | "$ref": "#/components/responses/default" 337 | } 338 | } 339 | }, 340 | "get": { 341 | "tags": ["admin"], 342 | "summary": "Get a category's settings", 343 | "description": "This route returns the category's current settings.", 344 | "parameters": [ 345 | { 346 | "name": "guild", 347 | "in": "path", 348 | "description": "The guild the category belongs to", 349 | "required": true, 350 | "style": "simple", 351 | "schema": { 352 | "type": "string" 353 | } 354 | }, 355 | { 356 | "name": "category", 357 | "in": "path", 358 | "description": "The category to get the settings of", 359 | "required": true, 360 | "style": "simple", 361 | "schema": { 362 | "type": "string" 363 | } 364 | } 365 | ], 366 | "responses": { 367 | "200": { 368 | "description": "OK", 369 | "content": { 370 | "application/json": { 371 | "schema": { 372 | "$ref": "#/components/schemas/CategorySettings" 373 | } 374 | } 375 | } 376 | }, 377 | "default": { 378 | "$ref": "#/components/responses/default" 379 | } 380 | } 381 | }, 382 | "put": { 383 | "tags": ["admin"], 384 | "summary": "Set a category's settings", 385 | "description": "This route sets the category's settings.", 386 | "parameters": [ 387 | { 388 | "name": "guild", 389 | "in": "path", 390 | "description": "The guild the category belongs to", 391 | "required": true, 392 | "style": "simple", 393 | "schema": { 394 | "type": "string" 395 | } 396 | }, 397 | { 398 | "name": "category", 399 | "in": "path", 400 | "description": "The category to set the settings of", 401 | "required": true, 402 | "style": "simple", 403 | "schema": { 404 | "type": "string" 405 | } 406 | } 407 | ], 408 | "requestBody": { 409 | "description": "The full CategorySettings object", 410 | "required": true, 411 | "content": { 412 | "application/json": { 413 | "schema": { 414 | "$ref": "#/components/schemas/CategorySettings" 415 | } 416 | } 417 | } 418 | }, 419 | "responses": { 420 | "200": { 421 | "description": "OK", 422 | "content": { 423 | "application/json": { 424 | "schema": { 425 | "type": "array", 426 | "items": { 427 | "$ref": "#/components/schemas/CategorySettings" 428 | } 429 | } 430 | } 431 | } 432 | }, 433 | "default": { 434 | "$ref": "#/components/responses/default" 435 | } 436 | } 437 | } 438 | }, 439 | "/admin/guilds/{guild}/stats": { 440 | "get": { 441 | "tags": ["admin"], 442 | "summary": "Get statistics about this guild", 443 | "description": "This route returns the statistics of the guild.", 444 | "parameters": [ 445 | { 446 | "name": "guild", 447 | "in": "path", 448 | "description": "The guild to get the stats of", 449 | "required": true, 450 | "style": "simple", 451 | "schema": { 452 | "type": "string" 453 | } 454 | } 455 | ], 456 | "responses": { 457 | "200": { 458 | "description": "OK", 459 | "content": { 460 | "application/json": { 461 | "schema": { 462 | "$ref": "#/components/schemas/GuildStats" 463 | } 464 | } 465 | } 466 | }, 467 | "default": { 468 | "$ref": "#/components/responses/default" 469 | } 470 | } 471 | } 472 | }, 473 | "/archives/guilds": { 474 | "get": { 475 | "tags": ["user"], 476 | "summary": "List the guilds that the client and the user have in common", 477 | "description": "This route returns an array of guilds that the client and user have in common and it is therefore possible for the user to have a ticket in.", 478 | "responses": { 479 | "200": { 480 | "description": "OK", 481 | "content": { 482 | "application/json": { 483 | "schema": { 484 | "type": "array", 485 | "items": { 486 | "$ref": "#/components/schemas/Guild" 487 | } 488 | } 489 | } 490 | } 491 | }, 492 | "default": { 493 | "$ref": "#/components/responses/default" 494 | } 495 | } 496 | } 497 | }, 498 | "/global/stats": { 499 | "get": { 500 | "tags": ["global"], 501 | "summary": "Get the statistics for this instance", 502 | "description": "This **public route** (no authentication required) returns the statistics of the bot instance.", 503 | "responses": { 504 | "200": { 505 | "description": "OK", 506 | "content": { 507 | "application/json": { 508 | "schema": { 509 | "$ref": "#/components/schemas/GlobalStats" 510 | } 511 | } 512 | } 513 | }, 514 | "default": { 515 | "$ref": "#/components/responses/default" 516 | } 517 | }, 518 | "security": [] 519 | } 520 | } 521 | }, 522 | "components": { 523 | "schemas": { 524 | "Error": { 525 | "type": "object", 526 | "properties": { 527 | "code": { 528 | "type": "integer" 529 | }, 530 | "message": { 531 | "type": "string" 532 | } 533 | }, 534 | "example": { 535 | "code": "0x190", 536 | "message": "Bad Request" 537 | } 538 | }, 539 | "Category": { 540 | "type": "object", 541 | "properties": { 542 | "id": { 543 | "type": "string" 544 | }, 545 | "name": { 546 | "type": "string" 547 | } 548 | }, 549 | "example": { 550 | "id": "620272351988285480", 551 | "name": "Support" 552 | } 553 | }, 554 | "CategorySettings": { 555 | "type": "object", 556 | "required": [ 557 | "channelName", 558 | "claiming", 559 | "discordCategory", 560 | "memberLimit", 561 | "name", 562 | "openingMessage", 563 | "pingRoles", 564 | "requiredRoles", 565 | "requireTopic", 566 | "staffRoles", 567 | "totalLimit" 568 | ], 569 | "properties": { 570 | "channelName": { 571 | "type": "string" 572 | }, 573 | "claiming": { 574 | "type": "boolean" 575 | }, 576 | "createdAt": { 577 | "type": "string" 578 | }, 579 | "description": { 580 | "type": "string" 581 | }, 582 | "discordCategory": { 583 | "type": "string" 584 | }, 585 | "emoji": { 586 | "type": "string" 587 | }, 588 | "enableFeedback": { 589 | "type": "boolean" 590 | }, 591 | "image": { 592 | "type": "string" 593 | }, 594 | "memberLimit": { 595 | "type": "integer" 596 | }, 597 | "name": { 598 | "type": "string" 599 | }, 600 | "openingMessage": { 601 | "type": "string" 602 | }, 603 | "pingRoles": { 604 | "type": "array", 605 | "items": { 606 | "type": "string" 607 | } 608 | }, 609 | "requiredRoles": { 610 | "type": "array", 611 | "items": { 612 | "type": "string" 613 | } 614 | }, 615 | "requireTopic": { 616 | "type": "boolean" 617 | }, 618 | "staffRoles": { 619 | "type": "array", 620 | "items": { 621 | "type": "string" 622 | } 623 | }, 624 | "surveyDescription": { 625 | "type": "string" 626 | }, 627 | "surveyLink": { 628 | "type": "string" 629 | }, 630 | "surveyTitle": { 631 | "type": "string" 632 | }, 633 | "totalLimit": { 634 | "type": "integer" 635 | } 636 | }, 637 | "example": { 638 | "channelName": "ticket-{num}", 639 | "claiming": false, 640 | "description": "Get help with anything", 641 | "discordCategory": "874366537727877181", 642 | "emoji": "❓", 643 | "image": "https://static.eartharoid.me/ticket-header.png", 644 | "memberLimit": 1, 645 | "name": "Support", 646 | "openingMessage": "Thank you for creating a ticket. Please be patient, a member of the support team will reply when they become available. Whilst you wait, please provide as much information about your support query as possible. ", 647 | "pingRoles": [513828182697312267], 648 | "requiredRoles": [], 649 | "requireTopic": true, 650 | "staffRoles": [451745787974778908, 513828182697312267], 651 | "surveyDescription": "Click on the button below to give us feedback.", 652 | "surveyLink": "https://forms.google.com", 653 | "surveyTitle": "How did we do?", 654 | "totalLimit": -1 655 | } 656 | }, 657 | "GlobalStats": { 658 | "type": "object", 659 | "properties": { 660 | "activatedUsers": { 661 | "type": "integer" 662 | }, 663 | "archviedMessages": { 664 | "type": "integer" 665 | }, 666 | "avgResponseTime": { 667 | "type": "integer" 668 | }, 669 | "guilds": { 670 | "type": "integer" 671 | }, 672 | "guildsAvgMembers": { 673 | "type": "integer" 674 | }, 675 | "guildsTotalMembers": { 676 | "type": "integer" 677 | }, 678 | "tags": { 679 | "type": "integer" 680 | }, 681 | "tickets": { 682 | "type": "integer" 683 | } 684 | } 685 | }, 686 | "Guild": { 687 | "type": "object", 688 | "properties": { 689 | "id": { 690 | "type": "string" 691 | }, 692 | "logo": { 693 | "type": "string" 694 | }, 695 | "name": { 696 | "type": "string" 697 | } 698 | }, 699 | "example": { 700 | "id": "451745464480432129", 701 | "logo": "https://cdn.discordapp.com/icons/451745464480432129/c340066806e27569c1c6b2bbd8ab28f1.png", 702 | "name": "Planet Earth" 703 | } 704 | }, 705 | "GuildStats": { 706 | "type": "object", 707 | "properties": { 708 | "categories": { 709 | "type": "array", 710 | "items": { 711 | "allOf": [ 712 | { 713 | "$ref": "#/components/schemas/Category" 714 | }, 715 | { 716 | "type": "object", 717 | "properties": { 718 | "tickets": { 719 | "type": "integer" 720 | } 721 | } 722 | } 723 | ] 724 | } 725 | }, 726 | "archviedMessages": { 727 | "type": "integer" 728 | }, 729 | "avgResponseTime": { 730 | "type": "string" 731 | }, 732 | "tags": { 733 | "type": "integer" 734 | }, 735 | "tickets": { 736 | "type": "integer" 737 | } 738 | }, 739 | "example": { 740 | "categories": [ 741 | { 742 | "id": "620272351988285480", 743 | "name": "Support", 744 | "tickets": 47 745 | } 746 | ], 747 | "archivedMessages": 1768, 748 | "avgResponseTime": "1h 7m", 749 | "tags": 14, 750 | "tickets": 47 751 | } 752 | }, 753 | "GuildSettings": { 754 | "type": "object", 755 | "required": ["archive", "blocklist", "errorColour", "primaryColour", "successColour"], 756 | "properties": { 757 | "archive": { 758 | "type": "boolean" 759 | }, 760 | "blocklist": { 761 | "type": "array", 762 | "items": { 763 | "type": "string" 764 | } 765 | }, 766 | "createdAt": { 767 | "type": "string" 768 | }, 769 | "errorColour": { 770 | "type": "string" 771 | }, 772 | "primaryColour": { 773 | "type": "string" 774 | }, 775 | "successColour": { 776 | "type": "string" 777 | } 778 | }, 779 | "example": { 780 | "archive": true, 781 | "blocklist": ["587112191950585856"], 782 | "errorColour": "RED", 783 | "primaryColour": "#009999", 784 | "successColour": "GREEN" 785 | } 786 | } 787 | }, 788 | "securitySchemes": { 789 | "api_key": { 790 | "type": "apiKey", 791 | "name": "Authorization", 792 | "in": "header" 793 | } 794 | }, 795 | "responses": { 796 | "default": { 797 | "description": "Unexpected error", 798 | "content": { 799 | "application/json": { 800 | "schema": { 801 | "$ref": "#/components/schemas/Error" 802 | } 803 | } 804 | } 805 | } 806 | } 807 | } 808 | } 809 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import forms from '@tailwindcss/forms'; 2 | import typography from '@tailwindcss/typography'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: ['./src/**/*.{html,js,svelte,ts}'], 7 | 8 | darkMode: 'class', 9 | 10 | theme: { 11 | extend: { 12 | colors: { 13 | blurple: '#5865F2', 14 | dgrey: { 15 | // brand "black" 23272A 16 | // very dark 1E1F22 17 | // darker 2B2D31 18 | // slightly dark 313338 19 | // not so dark 404249 20 | 950: '#1E1F22', 21 | 900: '#2B2D31', 22 | // 950: '#1E1F23', 23 | // 900: '#202225', 24 | 800: '#2f3136', 25 | 700: '#36393f', 26 | 600: '#4f545c', 27 | 400: '#d4d7dc', 28 | 300: '#e3e5e8', 29 | 200: '#ebedef', 30 | 100: '#f2f3f5' 31 | } 32 | } 33 | } 34 | }, 35 | 36 | variants: {}, 37 | 38 | plugins: [typography, forms] 39 | }; 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "checkJs": false, 5 | "verbatimModuleSyntax": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { I18nPlugin } from '@eartharoid/vite-plugin-i18n'; 3 | 4 | /** @type {import('vite').UserConfig} */ 5 | const config = { 6 | plugins: [ 7 | sveltekit(), 8 | I18nPlugin({ 9 | id_regex: /((?[a-z0-9-_]+)\/)((_(?[a-z0-9-_]+))|[a-z0-9-_]+)\.[a-z]+/i, 10 | include: 'src/lib/locales/*/*.json' 11 | }) 12 | ], 13 | server: { 14 | host: '127.0.0.1', 15 | proxy: { 16 | '/api': { 17 | target: 'http://127.0.0.1', 18 | changeOrigin: true 19 | }, 20 | '/attachments': { 21 | target: 'http://127.0.0.1', 22 | changeOrigin: true 23 | }, 24 | '/auth': { 25 | target: 'http://127.0.0.1', 26 | changeOrigin: true 27 | }, 28 | '/avatars': { 29 | target: 'http://127.0.0.1', 30 | changeOrigin: true 31 | }, 32 | '/invite': { 33 | target: 'http://127.0.0.1', 34 | changeOrigin: true 35 | } 36 | } 37 | } 38 | }; 39 | 40 | export default config; 41 | --------------------------------------------------------------------------------