Merged previous state
big bang v2? feat: finished with html (pretty much) started with css fix: fixed issues with the footer feat: added basic responsive design chore: moved to pnpm, improved footer feat: kinda finished homepage Init astro Add prettier Add import pahts Migrate Layout Move Files to new location Move Layout style Migrate Components Migrate all routes Added sass and migrated styles fix: moved scss and css to seperate file, fixed overflow chore: added folders to gitignore Signed-off-by: moonleay <contact@moonleay.net> feat: copyright notice now shows its clickableness chore: merged chore/fix-astro into master Add astro dependencies for build WIP: temp commit in case my laptop explodes WIP: fixed typos, added links, added text to about WIP: continued work on the feature page WIP: changes Chore: grid on features page WIP: further improvement of features page WIP: added CSS to how-do-i page, stack page, edited about page, edited links in footer, edited invite-link in header WIP: removed bg image from feature page WIP: improved css all around, started fixing features page fix: features page WIP: fixed typos on about page, wrapped acknowledgements with div, fixed typo in features, added new line in how-do-i footer, edited imprint formatting, updated privacy-policy and terms-of-service, updated about page css, styled acknowledgements page, added color to links on contact page, edited how-do-i style, added css to imprint, added css to privacy-policy, edited style of the stack page, added css to terms-of-service page feat: added docker support fix: centered image on stack page feat: added support server to contact page chore: edit privacy-policy formatting feat: added Discord Support server link into footer feat: made mobile screenshots appear on the mobile how-do-i page fix: fixed features page overflowing on chrome chore: bump version feat: improve footer Moved files to prepare for solid-start migration chore: squashed some commits, which are not for public viewing fix: fixed ImageSection being broken on mobile feat: added proper imprint feat: added name to privacy-policy feat: bump version fix: fixed footer not rendering correct on some mobile devices chore: bump version feat: imported html from Config repo Updated packages Migration from astro to solid-start Add database and auth Add discord rest testing Database schema rework API meeting progress Fix styles Add docker Add local assets Improved docker workflow feat: added styling to config page Fix: schema, move favicon and config page Update solid-start style: login and config working feat: complete last commit chore: Create Readme
8
.dockerignore
Normal file
|
@ -0,0 +1,8 @@
|
|||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.output
|
||||
.vinxi
|
||||
.git
|
8
.eslintrc.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"plugins": ["solid"],
|
||||
"extends": ["eslint:recommended", "plugin:solid/typescript"]
|
||||
}
|
29
.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
src/drizzle/migrations
|
||||
|
||||
dist
|
||||
.vinxi
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
netlify
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
*.launch
|
||||
.settings/
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
3
.prettierrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"plugins": ["prettier-plugin-organize-imports"]
|
||||
}
|
51
Dockerfile
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Use the desired base image
|
||||
FROM node:21-alpine AS base
|
||||
|
||||
# Set the NODE_ENV to production
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
# Pass the Font Awesome token as a build argument
|
||||
ARG FONT_AWESOME_TOKEN
|
||||
RUN echo "@fortawesome:registry=https://npm.fontawesome.com/" > ~/.npmrc \
|
||||
&& echo "//npm.fontawesome.com/:_authToken=${FONT_AWESOME_TOKEN}" >> ~/.npmrc \
|
||||
&& if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 solidjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=solidjs:nodejs /app/.output ./.output
|
||||
COPY --from=builder --chown=solidjs:nodejs /app/.vinxi ./.vinxi
|
||||
|
||||
USER solidjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Set the default values for environment variables
|
||||
ENV PORT 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD ["node", ".output/server/index.mjs"]
|
125
README.md
Normal file
|
@ -0,0 +1,125 @@
|
|||
# li'l Judd - Competitive Splatoon Bot
|
||||
|
||||
Welcome to li'l Judd, your go-to bot for enhancing your competitive Splatoon experience! Whether you're looking for match statistics, team coordination, or general Splatoon information, li'l Judd has got you covered.
|
||||
|
||||
## Features
|
||||
|
||||
- **Match Statistics:** Track and analyze your Splatoon matches to improve your gameplay.
|
||||
- **Team Coordination:** Plan strategies and coordinate with your team more effectively.
|
||||
- **General Splatoon Information:** Get information about weapons, maps, and game updates.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with li'l Judd, follow the instructions below.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js and pnpm installed (pnpm is strongly recommended for local development)
|
||||
- Docker (optional, for containerized deployment)
|
||||
- A Font Awesome subscription (required for the paid version used)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.moonleay.net/Websites/liljudd-website.git
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
cd lil-judd
|
||||
|
||||
# Install pnpm if not already installed
|
||||
npm i -g pnpm
|
||||
|
||||
# Add token to authenticate when using installing Font Awesome packages
|
||||
echo "@fortawesome:registry=https://npm.fontawesome.com/" > ~/.npmrc
|
||||
echo "//npm.fontawesome.com/:_authToken=<YOUR_TOKEN>" >> ~/.npmrc
|
||||
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Set up environment variables:
|
||||
|
||||
Create a `.env` file in the root directory and add the following variables:
|
||||
|
||||
```env
|
||||
VITE_DISCORD_CLIENT=your_discord_client_id
|
||||
VITE_DISCORD_CLIENT_SECRET=your_discord_client_secret
|
||||
VITE_DISCORD_BOT_TOKEN=your_discord_bot_token
|
||||
|
||||
VITE_AUTH_SECRET=your_auth_secret
|
||||
|
||||
VITE_DATABASE_URL=your_database_url
|
||||
```
|
||||
|
||||
#### Development
|
||||
|
||||
Specify `VITE_AUTH_REDIRECT_PROXY_URL` only if necessary, particularly when setting up a reverse proxy to test authentication with callbacks to your development box. [Auth.js Docs Reference](https://authjs.dev/reference/nextjs/#redirectproxyurl)
|
||||
|
||||
The duplicate `DATABASE_URL` is only needed for Drizzle Studio.
|
||||
|
||||
```
|
||||
VITE_AUTH_REDIRECT_PROXY_URL=your_auth_redirect_proxy_url
|
||||
|
||||
DATABASE_URL=your_database_url
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
- **Run development server:**
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
# Use --host if you have set up a reverse proxy to test auth with callback to your dev box
|
||||
pnpm dev --host
|
||||
```
|
||||
|
||||
- **Build for production:**
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
- **Start production server:**
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
- **Generate openapi types from specs:**
|
||||
|
||||
[openapi.json](https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json) (from discord-api-spec repo) -> [discord.d.ts](/src/types/discord.d.ts)
|
||||
|
||||
[liljudd.json](/public/api/specs/liljudd.json) -> [liljudd.d.ts](/src/types/liljudd.d.ts)
|
||||
|
||||
```bash
|
||||
pnpm discord-openapi-gen
|
||||
pnpm liljudd-openapi-gen
|
||||
```
|
||||
|
||||
**Drizzle Studio:**
|
||||
|
||||
Explore the database with ease through the intuitive web-based interface of Drizzle Studio, offering a visual and user-friendly way to interact with the data behind the scenes.
|
||||
|
||||
```bash
|
||||
pnpm drizzle-studio
|
||||
```
|
||||
|
||||
- **Docker Container:**
|
||||
|
||||
```bash
|
||||
docker build --build-arg FONT_AWESOME_TOKEN=<YOUR_TOKEN> -t $image_name .
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to contribute to li'l Judd, feel free to open an issue or submit a pull request. Your contributions are highly appreciated!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
|
||||
Happy splatting! 🦑🎮
|
25
discord_client_testing.http
Normal file
|
@ -0,0 +1,25 @@
|
|||
GET https://discord.com/api/users/@me
|
||||
Authorization: Bearer {{$dotenv DISCORD_ACCESS_TOKEN}}
|
||||
|
||||
###
|
||||
|
||||
GET https://discord.com/api/users/@me/guilds
|
||||
Authorization: Bearer {{$dotenv DISCORD_ACCESS_TOKEN}}
|
||||
|
||||
###
|
||||
|
||||
GET https://discord.com/api/users/@me/guilds/{{$dotenv DISCORD_GUILD_ID}}/member
|
||||
Authorization: Bearer {{$dotenv DISCORD_ACCESS_TOKEN}}
|
||||
|
||||
###
|
||||
|
||||
GET https://discord.com/api/guilds/{{$dotenv DISCORD_GUILD_ID}}
|
||||
Authorization: Bot {{$dotenv DISCORD_BOT_TOKEN}}
|
||||
|
||||
###
|
||||
|
||||
POST https://discord.com/api/oauth2/token/revoke
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Authorization: Basic {{$dotenv DISCORD_CLIENT_ID}}:{{$dotenv DISCORD_CLIENT_SECRET}}
|
||||
|
||||
token={{$dotenv DISCORD_ACCESS_TOKEN}}&token_type_hint=access_token
|
11
drizzle.config.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import "dotenv/config";
|
||||
import type { Config } from "drizzle-kit";
|
||||
|
||||
export default {
|
||||
schema: "./src/drizzle/schema.ts",
|
||||
out: "./src/drizzle/migrations",
|
||||
driver: "pg",
|
||||
dbCredentials: {
|
||||
connectionString: process.env.DATABASE_URL ?? "",
|
||||
},
|
||||
} satisfies Config;
|
55
package.json
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "liljudd-website",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vinxi dev",
|
||||
"build": "vinxi build",
|
||||
"start": "vinxi start",
|
||||
"lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\"",
|
||||
"push": "drizzle-kit push:pg",
|
||||
"discord-openapi-gen": "openapi-typescript https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json -o ./src/types/discord.d.ts",
|
||||
"liljudd-openapi-gen": "openapi-typescript ./public/api/specs/liljudd.json -o ./src/types/liljudd.d.ts",
|
||||
"typecheck": "tsc --noEmit --checkJs false --skipLibCheck --preserveSymLinks",
|
||||
"drizzle-studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.19.0",
|
||||
"@auth/drizzle-adapter": "^0.3.12",
|
||||
"@auth/solid-start": "0.1.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-light-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-thin-svg-icons": "^6.5.1",
|
||||
"@fortawesome/sharp-solid-svg-icons": "^6.5.1",
|
||||
"@solidjs/meta": "^0.29.2",
|
||||
"@solidjs/router": "^0.10.9",
|
||||
"@solidjs/start": "^0.4.9",
|
||||
"drizzle-orm": "^0.29.2",
|
||||
"moment-timezone": "^0.5.44",
|
||||
"openapi-fetch": "^0.8.2",
|
||||
"postgres": "^3.4.3",
|
||||
"solid-js": "^1.8.11",
|
||||
"vinxi": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-kit": "^0.20.9",
|
||||
"drizzle-zod": "^0.5.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-solid": "^0.13.1",
|
||||
"openapi-typescript": "^6.7.3",
|
||||
"pg": "^8.11.3",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"sass": "^1.69.6",
|
||||
"typescript": "^5.3.3",
|
||||
"zod": "3.22.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
7722
pnpm-lock.yaml
Normal file
34
public/api/index.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="SwaggerUI" />
|
||||
<title>SwaggerUI</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script
|
||||
src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js"
|
||||
crossorigin
|
||||
></script>
|
||||
<script
|
||||
src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-standalone-preset.js"
|
||||
crossorigin
|
||||
></script>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "/api/specs/liljudd.json",
|
||||
dom_id: "#swagger-ui",
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||
layout: "StandaloneLayout",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
238
public/api/specs/liljudd.json
Normal file
|
@ -0,0 +1,238 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "li'l Judd - OpenAPI 3.0",
|
||||
"description": "None yet",
|
||||
"termsOfService": "https://liljudd.ink/terms-of-service/",
|
||||
"contact": {
|
||||
"url": "https://liljudd.ink/contact/"
|
||||
},
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/api/config/{guildId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Guild configs"
|
||||
],
|
||||
"summary": "Find guild config by ID",
|
||||
"description": "Returns a single guild config",
|
||||
"operationId": "getGuildById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "guildId",
|
||||
"in": "path",
|
||||
"description": "ID of guild config to return",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/guildConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID supplied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Guild not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Guild configs"
|
||||
],
|
||||
"summary": "Deletes a guild config by ID",
|
||||
"description": "Delete a guild's config",
|
||||
"operationId": "deleteGuildById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "guildId",
|
||||
"in": "path",
|
||||
"description": "ID of guild config to delete",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "successful operation"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID supplied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Guild not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/tp_messages/{guildId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Time planning messages"
|
||||
],
|
||||
"summary": "Find guild by ID for it's tp_messages",
|
||||
"description": "Returns tp_messages for a guild",
|
||||
"operationId": "getTp_messagesOfGuildById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "guildId",
|
||||
"in": "path",
|
||||
"description": "ID of guild's tp_messages to return",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/guildConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"204": {
|
||||
"description": "Time planning not enabled for this guild"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID supplied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Guild not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"guildConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guildID": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time_planning": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channelID": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "0 0 1 * * * 60o 1w"
|
||||
},
|
||||
"isAvailableRoleId": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"wantsToBeNotifieRoledId": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)",
|
||||
"example": "1234567890123456789"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/match"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"match": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channelID": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"format": "varchar(50)",
|
||||
"example": "Scrim"
|
||||
},
|
||||
"createrId": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"roleId": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"opponentName": {
|
||||
"type": "string",
|
||||
"format": "varchar(100)",
|
||||
"example": "?"
|
||||
},
|
||||
"messsageId": {
|
||||
"type": "string",
|
||||
"format": "varchar(19)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "0 0 1 5 2 2023 60o"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"api_key": {
|
||||
"type": "apiKey",
|
||||
"name": "api_key",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
public/assets/bg.jpg
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
public/assets/fonts/Splatoon1-cjk-common.woff
Normal file
BIN
public/assets/fonts/Splatoon1-cjk-common.woff2
Normal file
BIN
public/assets/fonts/Splatoon1-common.woff
Normal file
BIN
public/assets/fonts/Splatoon1-common.woff2
Normal file
BIN
public/assets/fonts/Splatoon1-symbol-common.woff
Normal file
BIN
public/assets/fonts/Splatoon1-symbol-common.woff2
Normal file
BIN
public/assets/fonts/Splatoon1CHzh-level1.woff
Normal file
BIN
public/assets/fonts/Splatoon1CHzh-level1.woff2
Normal file
BIN
public/assets/fonts/Splatoon1CHzh-level2.woff
Normal file
BIN
public/assets/fonts/Splatoon1CHzh-level2.woff2
Normal file
BIN
public/assets/fonts/Splatoon1JP-hiragana-katakana.woff
Normal file
BIN
public/assets/fonts/Splatoon1JP-hiragana-katakana.woff2
Normal file
BIN
public/assets/fonts/Splatoon1JP-level1.woff
Normal file
BIN
public/assets/fonts/Splatoon1JP-level1.woff2
Normal file
BIN
public/assets/fonts/Splatoon1JP-level2.woff
Normal file
BIN
public/assets/fonts/Splatoon1JP-level2.woff2
Normal file
BIN
public/assets/fonts/Splatoon1KRko-level1.woff
Normal file
BIN
public/assets/fonts/Splatoon1KRko-level1.woff2
Normal file
BIN
public/assets/fonts/Splatoon1KRko-level2.woff
Normal file
BIN
public/assets/fonts/Splatoon1KRko-level2.woff2
Normal file
BIN
public/assets/fonts/Splatoon1TWzh-level1.woff
Normal file
BIN
public/assets/fonts/Splatoon1TWzh-level1.woff2
Normal file
BIN
public/assets/fonts/Splatoon1TWzh-level2.woff
Normal file
BIN
public/assets/fonts/Splatoon1TWzh-level2.woff2
Normal file
BIN
public/assets/fonts/Splatoon2-cjk-common.woff
Normal file
BIN
public/assets/fonts/Splatoon2-cjk-common.woff2
Normal file
BIN
public/assets/fonts/Splatoon2-common.woff
Normal file
BIN
public/assets/fonts/Splatoon2-common.woff2
Normal file
BIN
public/assets/fonts/Splatoon2-symbol-common.woff
Normal file
BIN
public/assets/fonts/Splatoon2-symbol-common.woff2
Normal file
BIN
public/assets/fonts/Splatoon2CHzh-level1.woff
Normal file
BIN
public/assets/fonts/Splatoon2CHzh-level1.woff2
Normal file
BIN
public/assets/fonts/Splatoon2CHzh-level2.woff
Normal file
BIN
public/assets/fonts/Splatoon2CHzh-level2.woff2
Normal file
BIN
public/assets/fonts/Splatoon2JP-hiragana-katakana.woff
Normal file
BIN
public/assets/fonts/Splatoon2JP-hiragana-katakana.woff2
Normal file
BIN
public/assets/fonts/Splatoon2JP-level1.woff
Normal file
BIN
public/assets/fonts/Splatoon2JP-level1.woff2
Normal file
BIN
public/assets/fonts/Splatoon2JP-level2.woff
Normal file
BIN
public/assets/fonts/Splatoon2JP-level2.woff2
Normal file
BIN
public/assets/fonts/Splatoon2KRko-level1.woff
Normal file
BIN
public/assets/fonts/Splatoon2KRko-level1.woff2
Normal file
BIN
public/assets/fonts/Splatoon2KRko-level2.woff
Normal file
BIN
public/assets/fonts/Splatoon2KRko-level2.woff2
Normal file
BIN
public/assets/fonts/Splatoon2TWzh-level1.woff
Normal file
BIN
public/assets/fonts/Splatoon2TWzh-level1.woff2
Normal file
BIN
public/assets/fonts/Splatoon2TWzh-level2.woff
Normal file
BIN
public/assets/fonts/Splatoon2TWzh-level2.woff2
Normal file
1
public/assets/icons/discord.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path fill="#ffffff" d="M524.5 69.8a1.5 1.5 0 0 0 -.8-.7A485.1 485.1 0 0 0 404.1 32a1.8 1.8 0 0 0 -1.9 .9 337.5 337.5 0 0 0 -14.9 30.6 447.8 447.8 0 0 0 -134.4 0 309.5 309.5 0 0 0 -15.1-30.6 1.9 1.9 0 0 0 -1.9-.9A483.7 483.7 0 0 0 116.1 69.1a1.7 1.7 0 0 0 -.8 .7C39.1 183.7 18.2 294.7 28.4 404.4a2 2 0 0 0 .8 1.4A487.7 487.7 0 0 0 176 479.9a1.9 1.9 0 0 0 2.1-.7A348.2 348.2 0 0 0 208.1 430.4a1.9 1.9 0 0 0 -1-2.6 321.2 321.2 0 0 1 -45.9-21.9 1.9 1.9 0 0 1 -.2-3.1c3.1-2.3 6.2-4.7 9.1-7.1a1.8 1.8 0 0 1 1.9-.3c96.2 43.9 200.4 43.9 295.5 0a1.8 1.8 0 0 1 1.9 .2c2.9 2.4 6 4.9 9.1 7.2a1.9 1.9 0 0 1 -.2 3.1 301.4 301.4 0 0 1 -45.9 21.8 1.9 1.9 0 0 0 -1 2.6 391.1 391.1 0 0 0 30 48.8 1.9 1.9 0 0 0 2.1 .7A486 486 0 0 0 610.7 405.7a1.9 1.9 0 0 0 .8-1.4C623.7 277.6 590.9 167.5 524.5 69.8zM222.5 337.6c-29 0-52.8-26.6-52.8-59.2S193.1 219.1 222.5 219.1c29.7 0 53.3 26.8 52.8 59.2C275.3 311 251.9 337.6 222.5 337.6zm195.4 0c-29 0-52.8-26.6-52.8-59.2S388.4 219.1 417.9 219.1c29.7 0 53.3 26.8 52.8 59.2C470.7 311 447.5 337.6 417.9 337.6z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
public/assets/icons/email.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path fill="#ffffff" d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></svg>
|
After Width: | Height: | Size: 529 B |
1
public/assets/icons/external.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path fill="#ffffff" d="M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32H320zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z"/></svg>
|
After Width: | Height: | Size: 682 B |
BIN
public/assets/lilJudd.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/logos/kord.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
public/assets/logos/kordextensions.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
18
public/assets/logos/kotlin.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<g id="Logotypes">
|
||||
<g>
|
||||
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="500.0035" y1="579.1058" x2="-9.653803e-02" y2="1079.2058" gradientTransform="matrix(0.9998 0 0 0.9998 9.651873e-02 -578.99)">
|
||||
<stop offset="3.435144e-03" style="stop-color:#E44857"/>
|
||||
<stop offset="0.4689" style="stop-color:#C711E1"/>
|
||||
<stop offset="1" style="stop-color:#7F52FF"/>
|
||||
</linearGradient>
|
||||
<polygon class="st0" points="500,500 0,500 0,0 500,0 250,250 "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 898 B |
BIN
public/assets/logos/pgelephant.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
public/assets/logox256.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
public/assets/screenshots/featureexample.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
public/assets/screenshots/featureexamplemobile.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
public/assets/screenshots/matchexample.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
public/assets/screenshots/matchexamplemobile.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
public/assets/screenshots/matchplanner.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
public/assets/screenshots/notifs.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
public/assets/screenshots/oldplanningmsg.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/assets/screenshots/rotationstatus.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
public/assets/screenshots/timeplanner.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
public/assets/screenshots/unknown.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 17 KiB |
32
sample_conf.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"data": {
|
||||
"guilds": [
|
||||
{
|
||||
"guildID": "some ID",
|
||||
"UTCOffset": 0,
|
||||
"features": {
|
||||
"time_planning": {
|
||||
"channelID": "some ID",
|
||||
"targetWeekday": 0,
|
||||
"targetHour": 0,
|
||||
"targetMinute": 0,
|
||||
"isAvailableRoleId": "some ID",
|
||||
"wantsToBeNotifieRoledId": "some ID"
|
||||
}
|
||||
},
|
||||
"matches": [
|
||||
{
|
||||
"channelID": "some ID",
|
||||
"matchType": "",
|
||||
"createrId": "some ID",
|
||||
"roleId": "some ID",
|
||||
"opponentName": "",
|
||||
"messsageId": "",
|
||||
"plannedFor": 1704314625000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessToken": "some Token"
|
||||
}
|
27
src/app.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
// @refresh reload
|
||||
import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||
import { Meta, MetaProvider, Title } from "@solidjs/meta";
|
||||
import { Router } from "@solidjs/router";
|
||||
import { FileRoutes } from "@solidjs/start";
|
||||
import { Suspense } from "solid-js";
|
||||
import "./styles/global.scss";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router
|
||||
root={(props) => (
|
||||
<MetaProvider>
|
||||
<Meta
|
||||
name="description"
|
||||
content="The Splatoon Discord bot with unique features."
|
||||
/>
|
||||
<Title>li'l Judd - Your competitive Splatoon assistant</Title>
|
||||
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MetaProvider>
|
||||
)}
|
||||
>
|
||||
<FileRoutes />
|
||||
</Router>
|
||||
);
|
||||
}
|
151
src/components/FontAwesomeIcon.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
import {
|
||||
FaSymbol,
|
||||
FlipProp,
|
||||
IconDefinition,
|
||||
IconProp,
|
||||
PullProp,
|
||||
RotateProp,
|
||||
SizeProp,
|
||||
Transform,
|
||||
} from "@fortawesome/fontawesome-svg-core";
|
||||
import { type JSX } from "solid-js";
|
||||
|
||||
export interface FontAwesomeIconProps
|
||||
extends Omit<
|
||||
JSX.SvgSVGAttributes<SVGSVGElement>,
|
||||
"children" | "mask" | "transform"
|
||||
> {
|
||||
icon: IconDefinition;
|
||||
mask?: IconProp;
|
||||
maskId?: string;
|
||||
color?: string;
|
||||
spin?: boolean;
|
||||
spinPulse?: boolean;
|
||||
spinReverse?: boolean;
|
||||
pulse?: boolean;
|
||||
beat?: boolean;
|
||||
fade?: boolean;
|
||||
beatFade?: boolean;
|
||||
bounce?: boolean;
|
||||
shake?: boolean;
|
||||
flash?: boolean;
|
||||
border?: boolean;
|
||||
fixedWidth?: boolean;
|
||||
inverse?: boolean;
|
||||
listItem?: boolean;
|
||||
flip?: FlipProp;
|
||||
size?: SizeProp;
|
||||
pull?: PullProp;
|
||||
rotation?: RotateProp;
|
||||
transform?: string | Transform;
|
||||
symbol?: FaSymbol;
|
||||
style?: JSX.CSSProperties;
|
||||
tabIndex?: number;
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
swapOpacity?: boolean;
|
||||
}
|
||||
|
||||
const idPool = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
function nextUniqueId() {
|
||||
let size = 12;
|
||||
let id = "";
|
||||
|
||||
while (size-- > 0) {
|
||||
id += idPool[(Math.random() * 62) | 0];
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function Path(props: { d: string | string[] }) {
|
||||
return (
|
||||
<>
|
||||
{typeof props.d === "string" ? (
|
||||
<path fill="currentColor" d={props.d} />
|
||||
) : (
|
||||
<>
|
||||
<path class="fa-secondary" fill="currentColor" d={props.d[0]} />
|
||||
<path class="fa-primary" fill="currentColor" d={props.d[1]} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FontAwesomeIcon(props: FontAwesomeIconProps) {
|
||||
const titleId = () =>
|
||||
props.title
|
||||
? "svg-inline--fa-title-".concat(props.titleId || nextUniqueId())
|
||||
: undefined;
|
||||
// Get CSS class list from the props object
|
||||
function attributes() {
|
||||
const defaultClasses = {
|
||||
"svg-inline--fa": true,
|
||||
[`fa-${props.icon.iconName}`]: true,
|
||||
[props.class ?? ""]:
|
||||
typeof props.class !== "undefined" && props.class !== null,
|
||||
...props.classList,
|
||||
};
|
||||
|
||||
// map of CSS class names to properties
|
||||
const faClasses = {
|
||||
"fa-beat": props.beat,
|
||||
"fa-fade": props.fade,
|
||||
"fa-beat-fade": props.beatFade,
|
||||
"fa-bounce": props.bounce,
|
||||
"fa-shake": props.shake,
|
||||
"fa-flash": props.flash,
|
||||
"fa-spin": props.spin,
|
||||
"fa-spin-reverse": props.spinReverse,
|
||||
"fa-spin-pulse": props.spinPulse,
|
||||
"fa-pulse": props.pulse,
|
||||
"fa-fw": props.fixedWidth,
|
||||
"fa-inverse": props.inverse,
|
||||
"fa-border": props.border,
|
||||
"fa-li": props.listItem,
|
||||
"fa-flip": typeof props.flip !== "undefined" && props.flip !== null,
|
||||
"fa-flip-horizontal":
|
||||
props.flip === "horizontal" || props.flip === "both",
|
||||
"fa-flip-vertical": props.flip === "vertical" || props.flip === "both",
|
||||
[`fa-${props.size}`]:
|
||||
typeof props.size !== "undefined" && props.size !== null,
|
||||
[`fa-rotate-${props.rotation}`]:
|
||||
typeof props.rotation !== "undefined" && props.size !== null,
|
||||
[`fa-pull-${props.pull}`]:
|
||||
typeof props.pull !== "undefined" && props.pull !== null,
|
||||
"fa-swap-opacity": props.swapOpacity,
|
||||
};
|
||||
|
||||
const attributes = {
|
||||
focusable: !!props.title,
|
||||
"aria-hidden": !props.title,
|
||||
role: "img",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
"aria-labelledby": titleId(),
|
||||
"data-prefix": props.icon.prefix,
|
||||
"data-icon": props.icon.iconName,
|
||||
"data-fa-transform": props.transform,
|
||||
"data-fa-mask": props.mask,
|
||||
"data-fa-mask-id": props.maskId,
|
||||
"data-fa-symbol": props.symbol,
|
||||
tabIndex: props.tabIndex,
|
||||
classList: { ...defaultClasses, ...faClasses },
|
||||
color: props.color,
|
||||
style: props.style,
|
||||
viewBox: `0 0 ${props.icon.icon[0]} ${props.icon.icon[1]}`,
|
||||
} as const;
|
||||
|
||||
// return the complete class list
|
||||
return attributes;
|
||||
}
|
||||
|
||||
return (
|
||||
<svg {...attributes()}>
|
||||
{/* <Show when={props.title}>
|
||||
<title id={titleId()}>{props.title}</title>
|
||||
</Show> */}
|
||||
<Path d={props.icon.icon[4]} />
|
||||
</svg>
|
||||
);
|
||||
}
|
108
src/components/Footer.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
import "../styles/components/Footer.scss";
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer>
|
||||
<div class="footerIcon">
|
||||
<img
|
||||
class="footerImage"
|
||||
src="/assets/lilJudd.png"
|
||||
alt="A cute Image of lil Judd <3"
|
||||
/>
|
||||
<p>li'l Judd</p>
|
||||
<p class="footerImageNote">Your competitive Splatoon assistant.</p>
|
||||
</div>
|
||||
<div class="footerTable">
|
||||
<div>
|
||||
<h3>Navigation</h3>
|
||||
<div class="footerLinks">
|
||||
<a href="/">Home</a>
|
||||
<a href="/features">Features</a>
|
||||
<a href="/how-do-i">How do I...?</a>
|
||||
<a href="/stack">The Stack</a>
|
||||
<a href="/about">About</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>The Software</h3>
|
||||
<div class="footerLinks">
|
||||
<a
|
||||
href="https://git.moonleay.net/DiscordBots/lilJudd"
|
||||
target="_blank"
|
||||
>
|
||||
The code of the bot{" "}
|
||||
<img src="/assets/icons/external.svg" alt="external link" />
|
||||
</a>
|
||||
<a
|
||||
href="https://git.moonleay.net/Websites/liljudd-website"
|
||||
target="_blank"
|
||||
>
|
||||
The code of the website{" "}
|
||||
<img src="/assets/icons/external.svg" alt="external link" />
|
||||
</a>
|
||||
<a
|
||||
href="https://todo.moonleay.net/share/OmisuzgPDdsrCAXKjGrTfYzWwqNDNclOMGJWeMsi/auth?view=kanban"
|
||||
target="_blank"
|
||||
>
|
||||
The todo list{" "}
|
||||
<img src="/assets/icons/external.svg" alt="external link" />
|
||||
</a>
|
||||
<a href="/acknowledgements">Acknowledgements</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>More</h3>
|
||||
<div class="footerLinks">
|
||||
<a href="https://moonleay.net/">
|
||||
My homepage{" "}
|
||||
<img src="/assets/icons/external.svg" alt="external link" />
|
||||
</a>
|
||||
<a href="https://moonleay.net/blog/" target="_blank">
|
||||
My blog{" "}
|
||||
<img src="/assets/icons/external.svg" alt="external link" />
|
||||
</a>
|
||||
<a href="/contact">Contact me</a>
|
||||
<a href="https://status.moonleay.net/" target="_blank">
|
||||
Server Status{" "}
|
||||
<img src="/assets/icons/external.svg" alt="external link" />
|
||||
</a>
|
||||
<a href="https://discord.gg/HTZRktfH4A" target="_blank">
|
||||
Support Discord{" "}
|
||||
<img src="/assets/icons/external.svg" alt="external link" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Legal</h3>
|
||||
<div class="footerLinks">
|
||||
<a href="/imprint">Imprint</a>
|
||||
<a href="/privacy-policy">Privacy Policy</a>
|
||||
<a href="/terms-of-service">Terms of Service</a>
|
||||
<a
|
||||
href="https://git.moonleay.net/DiscordBots/lilJudd/src/branch/master/LICENSE"
|
||||
target="_blank"
|
||||
>
|
||||
The license{" "}
|
||||
<img src="/assets/icons/external.svg" alt="external link" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footerNotice">
|
||||
<p>
|
||||
This website is NOT affiliated with Nintendo or any other party. All
|
||||
product names, logos, and brands are property of their respective
|
||||
owners.
|
||||
</p>
|
||||
<p>
|
||||
li'l Judd © 2022-2024{" "}
|
||||
<a href="https://moonleay.net/" target="_blank">
|
||||
moonleay
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
25
src/components/ImageSection.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import "../styles/components/ImageSection.scss";
|
||||
|
||||
interface Props {
|
||||
imgUrl: string;
|
||||
imgAlt: string;
|
||||
title: string;
|
||||
description: string;
|
||||
note: string;
|
||||
span?: boolean;
|
||||
}
|
||||
|
||||
function ImageSection(props: Props) {
|
||||
return (
|
||||
<section classList={{ ImageSection: true, span: props.span ?? true }}>
|
||||
<h1>{props.title}</h1>
|
||||
<div class="imgDiv">
|
||||
<img src={props.imgUrl} alt={props.imgAlt} />
|
||||
<p>{props.description}</p>
|
||||
</div>
|
||||
<p class="noteP">{props.note}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageSection;
|
18
src/components/Layout.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { JSX, Suspense } from "solid-js";
|
||||
import "../styles/Layout.scss";
|
||||
import Footer from "./Footer";
|
||||
import NavBar from "./NavBar";
|
||||
|
||||
function Layout(props: { children: JSX.Element; site: string }) {
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<div class={props.site}>
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
56
src/components/NavBar.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { faCirclePlus } from "@fortawesome/pro-regular-svg-icons";
|
||||
import { JSX, Show, Suspense } from "solid-js";
|
||||
import "../styles/components/NavBar.scss";
|
||||
import { FontAwesomeIcon } from "./FontAwesomeIcon";
|
||||
import NavUser from "./NavUser";
|
||||
|
||||
export function Li(props: {
|
||||
href: string;
|
||||
action?: () => void;
|
||||
name?: string;
|
||||
children?: JSX.Element;
|
||||
}) {
|
||||
return (
|
||||
<li class="navElem flex-row thick">
|
||||
<a
|
||||
class="flex-row"
|
||||
href={props.href}
|
||||
onClick={() => props.action && props.action()}
|
||||
>
|
||||
{props.children ?? <></>}
|
||||
<Show when={props.name}>
|
||||
<span>{props.name}</span>
|
||||
</Show>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function NavBar() {
|
||||
return (
|
||||
<nav class="flex-row responsive">
|
||||
<ul class="flex-row responsive thick">
|
||||
<Li href="/" name="li'l Judd">
|
||||
<img src="/assets/logox256.png" alt="The Bots Logo" />
|
||||
</Li>
|
||||
<Li href="/features" name="Features" />
|
||||
<Li href="/how-do-i" name="How do I...?" />
|
||||
<Li href="/stack" name="The Stack" />
|
||||
<Li href="/about" name="About" />
|
||||
</ul>
|
||||
<ul class="flex-row responsive thick">
|
||||
<Li
|
||||
href="https://discord.com/api/oauth2/authorize?client_id=1024410658973941862&permissions=18977581952080&scope=bot"
|
||||
name="Invite to your server"
|
||||
>
|
||||
<FontAwesomeIcon class="lower" icon={faCirclePlus} size="xl" />
|
||||
</Li>
|
||||
<Suspense>
|
||||
<NavUser />
|
||||
</Suspense>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBar;
|
87
src/components/NavUser.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { getSession } from "@auth/solid-start";
|
||||
import { signIn, signOut } from "@auth/solid-start/client";
|
||||
import {
|
||||
faArrowRightFromBracket,
|
||||
faArrowRightToBracket,
|
||||
faGear,
|
||||
} from "@fortawesome/pro-regular-svg-icons";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Show, createResource } from "solid-js";
|
||||
import { getRequestEvent } from "solid-js/web";
|
||||
import db from "~/drizzle";
|
||||
import { users } from "~/drizzle/schema";
|
||||
import { authOptions } from "~/server/auth";
|
||||
import { FontAwesomeIcon } from "./FontAwesomeIcon";
|
||||
import { Li } from "./NavBar";
|
||||
|
||||
const initialUser = {
|
||||
id: "",
|
||||
name: null as string | null,
|
||||
email: "",
|
||||
emailVerified: null as Date | null,
|
||||
image: null as string | null,
|
||||
};
|
||||
|
||||
async function getUser() {
|
||||
"use server";
|
||||
|
||||
const event = getRequestEvent();
|
||||
if (!event)
|
||||
return { success: false, message: "No request event!", ...initialUser };
|
||||
|
||||
const session = await getSession(event.request, authOptions);
|
||||
if (!session?.user?.id)
|
||||
return { success: false, message: "No user with id!", ...initialUser };
|
||||
|
||||
const user = (
|
||||
await db
|
||||
.selectDistinct()
|
||||
.from(users)
|
||||
.where(eq(users.id, session.user?.id))
|
||||
.limit(1)
|
||||
.execute()
|
||||
)[0];
|
||||
|
||||
return { success: true, message: "", ...user };
|
||||
}
|
||||
|
||||
function NavUser() {
|
||||
const [user] = createResource(() =>
|
||||
getUser().then((e) => {
|
||||
if (!e.success) console.log(1, e.message);
|
||||
console.log(2, e);
|
||||
return e;
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={user()?.id}
|
||||
fallback={
|
||||
<Li
|
||||
href="#"
|
||||
name="Login"
|
||||
action={() => signIn("discord", { callbackUrl: "/config" })}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
class="secondary"
|
||||
icon={faArrowRightToBracket}
|
||||
size="xl"
|
||||
/>
|
||||
</Li>
|
||||
}
|
||||
>
|
||||
<Li href="/config">
|
||||
<div class="swap lower">
|
||||
<img class="primary" src={user()?.image ?? ""} alt="User pfp" />
|
||||
<FontAwesomeIcon class="secondary" icon={faGear} size="xl" />
|
||||
</div>
|
||||
</Li>
|
||||
<Li href="#" action={() => signOut({ callbackUrl: "/" })}>
|
||||
<FontAwesomeIcon icon={faArrowRightFromBracket} size="xl" />
|
||||
</Li>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavUser;
|
10
src/drizzle/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const queryClient = postgres(import.meta.env.VITE_DATABASE_URL ?? "");
|
||||
const db = drizzle(queryClient, {
|
||||
schema,
|
||||
});
|
||||
|
||||
export default db;
|
140
src/drizzle/schema.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
import type { AdapterAccount } from "@auth/core/adapters";
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
integer,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
serial,
|
||||
smallint,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable("user", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
name: text("name"),
|
||||
email: text("email").notNull(),
|
||||
emailVerified: timestamp("emailVerified", { mode: "date" }),
|
||||
image: text("image"),
|
||||
});
|
||||
|
||||
export const accounts = pgTable(
|
||||
"account",
|
||||
{
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
type: text("type").$type<AdapterAccount["type"]>().notNull(),
|
||||
provider: text("provider").notNull(),
|
||||
providerAccountId: text("providerAccountId").notNull(),
|
||||
refresh_token: text("refresh_token"),
|
||||
access_token: text("access_token"),
|
||||
expires_at: integer("expires_at"),
|
||||
token_type: text("token_type"),
|
||||
scope: text("scope"),
|
||||
id_token: text("id_token"),
|
||||
session_state: text("session_state"),
|
||||
},
|
||||
(account) => ({
|
||||
compoundKey: primaryKey({
|
||||
columns: [account.provider, account.providerAccountId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const sessions = pgTable("session", {
|
||||
sessionToken: text("sessionToken").notNull().primaryKey(),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
||||
});
|
||||
|
||||
export const verificationTokens = pgTable(
|
||||
"verificationToken",
|
||||
{
|
||||
identifier: text("identifier").notNull(),
|
||||
token: text("token").notNull(),
|
||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
||||
},
|
||||
(vt) => ({
|
||||
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
||||
}),
|
||||
);
|
||||
|
||||
export const matchPlannings = pgTable("match_planning", {
|
||||
id: serial("id").primaryKey(),
|
||||
channelId: varchar("channel_id", { length: 20 }).notNull(),
|
||||
matchtype: varchar("match_type", { length: 50 }).notNull(),
|
||||
createrId: varchar("creater_id", { length: 20 }).notNull(),
|
||||
roleId: varchar("role_id", { length: 20 }).notNull(),
|
||||
opponentName: varchar("opponent_name", { length: 100 }).notNull(),
|
||||
messageId: varchar("message_id", { length: 20 }).notNull(),
|
||||
ts: timestamp("ts").notNull(),
|
||||
guildId: varchar("guild_id", { length: 20 })
|
||||
.notNull()
|
||||
.references(() => guilds.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const matchPlanningsRelations = relations(matchPlannings, ({ one }) => ({
|
||||
guild: one(guilds, {
|
||||
fields: [matchPlannings.guildId],
|
||||
references: [guilds.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const guilds = pgTable("guild", {
|
||||
id: varchar("id", { length: 20 }).primaryKey(),
|
||||
timezone: text("timezone").notNull(),
|
||||
});
|
||||
|
||||
export const guildsRelations = relations(guilds, ({ one, many }) => ({
|
||||
matches: many(matchPlannings),
|
||||
timePlanning: one(timePlannings, {
|
||||
fields: [guilds.id],
|
||||
references: [timePlannings.guildId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const timePlannings = pgTable("time_planning", {
|
||||
id: serial("id").primaryKey(),
|
||||
guildId: varchar("guild_id", { length: 20 })
|
||||
.notNull()
|
||||
.unique()
|
||||
.references(() => guilds.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
channelId: varchar("channel_id", { length: 20 }).notNull(),
|
||||
target_interval: smallint("target_interval").notNull(),
|
||||
isAvailableRoleId: varchar("is_available_role_id", { length: 20 }),
|
||||
wantsToBeNotifieRoledId: varchar("wants_to_be_notified_role_id", {
|
||||
length: 20,
|
||||
}),
|
||||
});
|
||||
|
||||
export const timePlanningsRelations = relations(
|
||||
timePlannings,
|
||||
({ one, many }) => ({
|
||||
guild: one(guilds, {
|
||||
fields: [timePlannings.guildId],
|
||||
references: [guilds.id],
|
||||
}),
|
||||
messages: many(tpMessages),
|
||||
}),
|
||||
);
|
||||
|
||||
export const tpMessages = pgTable("tp_message", {
|
||||
messageId: varchar("message_id", { length: 20 }).primaryKey(),
|
||||
day: smallint("day").notNull(),
|
||||
planId: integer("plan_id")
|
||||
.notNull()
|
||||
.references(() => timePlannings.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const tpMessagesRelations = relations(tpMessages, ({ one }) => ({
|
||||
plan: one(timePlannings, {
|
||||
fields: [tpMessages.planId],
|
||||
references: [timePlannings.id],
|
||||
}),
|
||||
}));
|
3
src/entry-client.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { mount, StartClient } from "@solidjs/start/client";
|
||||
|
||||
mount(() => <StartClient />, document.getElementById("app"));
|
21
src/entry-server.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { createHandler } from "@solidjs/start/entry";
|
||||
import { StartServer } from "@solidjs/start/server";
|
||||
|
||||
export default createHandler(() => (
|
||||
<StartServer
|
||||
document={({ assets, children, scripts }) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
{assets}
|
||||
</head>
|
||||
<body id="app">
|
||||
{children}
|
||||
{scripts}
|
||||
</body>
|
||||
</html>
|
||||
)}
|
||||
/>
|
||||
));
|
1
src/global.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="@solidjs/start/env" />
|
23
src/routes/[...404].tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Title } from "@solidjs/meta";
|
||||
import { HttpStatusCode } from "@solidjs/start";
|
||||
import Layout from "~/components/Layout";
|
||||
import "../styles/pages/index.scss";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<Layout site="index">
|
||||
<Title>Not Found</Title>
|
||||
<HttpStatusCode code={404} />
|
||||
<section class="index">
|
||||
<h1>404 - Whoops, ink spill!</h1>
|
||||
<h5>li'l Judd couldn't find this turf.</h5>
|
||||
<div>
|
||||
<p class="p1">
|
||||
Head back to <a href="/">base</a> and splat into action from there!
|
||||
🦑🕹️
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
);
|
||||
}
|
78
src/routes/about.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import Layout from "~/components/Layout";
|
||||
import "../styles/pages/about.scss";
|
||||
|
||||
function about() {
|
||||
return (
|
||||
<Layout site="about">
|
||||
<h1>About</h1>
|
||||
<section>
|
||||
<h2>Why does this bot exist?</h2>
|
||||
<p>
|
||||
We had a person in our team, who sent{" "}
|
||||
<a href="/assets/screenshots/oldplanningmsg.png" target="_blank">
|
||||
these planning messages
|
||||
</a>{" "}
|
||||
and I thought that this should be automated. Some time later the first
|
||||
version of li'l Judd was born. Today the bot has more features and
|
||||
keeps getting more of them! It is designed to actually improve the
|
||||
Splatoon experience and not be the 10000th moderation and general
|
||||
utility bot with the same features as all bots.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Who is behind this?</h2>
|
||||
<p>
|
||||
The bot is currently being developed by{" "}
|
||||
<a href="/contact">moonleay</a> (hey that's me!) with occasional
|
||||
help from his friends!
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>How can I trust you?</h2>
|
||||
<p>
|
||||
The bot only requests permissions, which are needed for it to work.
|
||||
Additionally, if you want to check how the bot works under the hood,
|
||||
you can
|
||||
<a href="https://git.moonleay.net/DiscordBots/lilJudd">
|
||||
read the code
|
||||
</a>
|
||||
and if you still don't trust me, you can always host the bot yourself!
|
||||
A guide on how to do that can be found in the README of the git
|
||||
project.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Where is my data stored?</h2>
|
||||
<p>
|
||||
Your data is stored on a VPS from Contabo in Germany. The bot used to
|
||||
be hosted on a server in my basement, but I moved it to a VPS, because
|
||||
my internet connection was not stable enough.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>So whats in the future?</h2>
|
||||
<p>
|
||||
I plan on adding features, which are aimed to improve your and your
|
||||
teams competitive experience! You can check out my public todo list{" "}
|
||||
<a
|
||||
href="https://todo.moonleay.net/share/OmisuzgPDdsrCAXKjGrTfYzWwqNDNclOMGJWeMsi/auth?view=kanban"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Hey, there is this really cool idea I have! Can you add it?</h2>
|
||||
<p>
|
||||
Just message me! I can't promise anything, but I am always open to new
|
||||
ideas and improvements! You can find ways to contact me{" "}
|
||||
<a href="/contact">here</a>.
|
||||
</p>
|
||||
</section>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default about;
|
196
src/routes/acknowledgements.tsx
Normal file
|
@ -0,0 +1,196 @@
|
|||
import Layout from "~/components/Layout";
|
||||
import "../styles/pages/acknowledgements.scss";
|
||||
|
||||
function acknowledgements() {
|
||||
return (
|
||||
<Layout site="acknowledgements">
|
||||
<h1>Acknowledgements</h1>
|
||||
<section>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<p>tool</p>
|
||||
</th>
|
||||
<th>
|
||||
<p>license</p>
|
||||
</th>
|
||||
<th>
|
||||
<p>page</p>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Kotlin</p>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://kotlinlang.org/docs/faq.html#is-kotlin-free"
|
||||
target="_blank"
|
||||
>
|
||||
Apache license 2.0
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://kotlinlang.org/" target="_blank">
|
||||
website
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Kord</p>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://github.com/kordlib/kord/blob/main/LICENSE"
|
||||
target="_blank"
|
||||
>
|
||||
MIT license
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://kord.dev/" target="_blank">
|
||||
website
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Kord Extensions</p>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://github.com/Kord-Extensions/kord-extensions/blob/root/LICENSE"
|
||||
target="_blank"
|
||||
>
|
||||
Mozilla Public License 2.0
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://kordex.kotlindiscord.com/" target="_blank">
|
||||
website
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>PostgreSQL</p>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://www.postgresql.org/about/licence/"
|
||||
target="_blank"
|
||||
>
|
||||
PostgreSQL license
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.postgresql.org/" target="_blank">
|
||||
website
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>kotlinx-coroutines-core</p>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/LICENSE.txt"
|
||||
target="_blank"
|
||||
>
|
||||
Apache license 2.0
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://github.com/Kotlin/kotlinx.coroutines"
|
||||
target="_blank"
|
||||
>
|
||||
repo
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>slf4j</p>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt"
|
||||
target="_blank"
|
||||
>
|
||||
MIT license
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.slf4j.org/" target="_blank">
|
||||
website
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Exposed</p>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt">
|
||||
Apache license 2.0
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://github.com/JetBrains/Exposed" target="_blank">
|
||||
repo
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Krontab</p>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://github.com/InsanusMokrassar/krontab/blob/master/LICENSE"
|
||||
target="_blank"
|
||||
>
|
||||
Apache license 2.0
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://github.com/InsanusMokrassar/krontab"
|
||||
target="_blank"
|
||||
>
|
||||
repo
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Splatoon3.ink</p>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://github.com/misenhower/splatoon3.ink/blob/main/license.md"
|
||||
target="_blank"
|
||||
>
|
||||
MIT License
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://splatoon3.ink/" target="_blank">
|
||||
website
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default acknowledgements;
|
4
src/routes/api/auth/[...solidauth].ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { SolidAuth } from "@auth/solid-start"
|
||||
import { authOptions } from "~/server/auth"
|
||||
|
||||
export const { GET, POST } = SolidAuth(authOptions)
|
271
src/routes/config/[guildId].tsx
Normal file
|
@ -0,0 +1,271 @@
|
|||
import { getSession } from "@auth/solid-start";
|
||||
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
|
||||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
import { eq } from "drizzle-orm";
|
||||
import moment from "moment-timezone";
|
||||
import createClient from "openapi-fetch";
|
||||
import { Index, createEffect, createResource, createSignal } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { getRequestEvent } from "solid-js/web";
|
||||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||
import Layout from "~/components/Layout";
|
||||
import db from "~/drizzle";
|
||||
import { accounts } from "~/drizzle/schema";
|
||||
import { authOptions } from "~/server/auth";
|
||||
import { paths } from "~/types/discord";
|
||||
import "../../styles/pages/config.scss";
|
||||
|
||||
const guessTZ = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
const initialValue = (params: ReturnType<typeof useParams>) => ({
|
||||
success: null as boolean | null,
|
||||
guild: {
|
||||
id: params.guildId,
|
||||
name: undefined as string | undefined,
|
||||
icon: undefined as string | null | undefined,
|
||||
},
|
||||
tzNames: [guessTZ()],
|
||||
});
|
||||
|
||||
const getPayload = async (
|
||||
id: string,
|
||||
): Promise<
|
||||
| { success: false; message: string }
|
||||
| (ReturnType<typeof initialValue> & { success: true })
|
||||
> => {
|
||||
"use server";
|
||||
const event = getRequestEvent();
|
||||
if (!event) return { success: false, message: "No request event!" };
|
||||
|
||||
const session = await getSession(event.request, authOptions);
|
||||
if (!session?.user?.id)
|
||||
return { success: false, message: "No user with id!" };
|
||||
|
||||
const { DISCORD_ACCESS_TOKEN } = (
|
||||
await db
|
||||
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
|
||||
.from(accounts)
|
||||
.where(eq(accounts.userId, session.user?.id))
|
||||
.limit(1)
|
||||
.execute()
|
||||
)[0];
|
||||
if (!DISCORD_ACCESS_TOKEN)
|
||||
return { success: false, message: "No discord access token!" };
|
||||
|
||||
// const guilds = await fetch("https://discord.com/api/users/@me/guilds", {
|
||||
// headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
|
||||
// }).then((res) => res.json());
|
||||
const { GET } = createClient<paths>({
|
||||
baseUrl: "https://discord.com/api/v10",
|
||||
});
|
||||
const { data: guilds, error } = await GET("/users/@me/guilds", {
|
||||
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
return { success: false, message: "Error on discord api request!" };
|
||||
}
|
||||
|
||||
const guild = guilds?.find((e) => e.id === id);
|
||||
|
||||
if (!guild)
|
||||
return {
|
||||
success: false,
|
||||
message: "User is in no such guild with requested id!",
|
||||
};
|
||||
if (!(parseInt(guild.permissions) & (1 << 5)))
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"User is no MANAGE_GUILD permissions on this guild with requested id!",
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
guild: {
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
icon: guild.icon,
|
||||
},
|
||||
// guild: guilds
|
||||
// .filter((e: any) => e.permissions & (1 << 5))
|
||||
// .map((e: any) => e.name),
|
||||
tzNames: moment.tz.names(),
|
||||
};
|
||||
};
|
||||
|
||||
function config() {
|
||||
const params = useParams();
|
||||
const navigator = useNavigate();
|
||||
let timezoneRef: HTMLInputElement;
|
||||
let timePlanningRef: HTMLInputElement;
|
||||
let pingableRolesRef: HTMLInputElement;
|
||||
|
||||
const [timezone, setTimezone] = createSignal(guessTZ());
|
||||
const [payload] = createResource(params.guildId, async (id) => {
|
||||
const payload = await getPayload(id);
|
||||
|
||||
if (!payload.success) {
|
||||
console.log(payload.message, "No success");
|
||||
navigator("/config", { replace: false });
|
||||
return initialValue(params);
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
const guild = () => payload()?.guild ?? initialValue(params).guild;
|
||||
const tzNames = () => payload()?.tzNames ?? [];
|
||||
const [config, setConfig] = createStore({
|
||||
features: {
|
||||
timePlanning: {
|
||||
enabled: false,
|
||||
pingableRoles: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createEffect(() => console.log(payload()));
|
||||
createEffect(() => console.log("timezone", timezone()));
|
||||
createEffect(() =>
|
||||
console.log("timePlanning.enabled", config.features.timePlanning.enabled),
|
||||
);
|
||||
createEffect(() =>
|
||||
console.log(
|
||||
"timePlanning.pingableRoles",
|
||||
config.features.timePlanning.pingableRoles,
|
||||
),
|
||||
);
|
||||
|
||||
createEffect(() => (timezoneRef.value = timezone()));
|
||||
createEffect(
|
||||
() => (timePlanningRef.checked = config.features.timePlanning.enabled),
|
||||
);
|
||||
createEffect(
|
||||
() =>
|
||||
(pingableRolesRef.checked = config.features.timePlanning.pingableRoles),
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout site="config">
|
||||
<h3 class="text-center">Configure li'l Judd in</h3>
|
||||
<div>
|
||||
<div>
|
||||
<div class="flex-row centered">
|
||||
<img
|
||||
class="guildpfp"
|
||||
src={
|
||||
guild()?.icon
|
||||
? `https://cdn.discordapp.com/icons/${guild()?.id}/${guild()
|
||||
?.icon}.webp?size=240`
|
||||
: "https://cdn.discordapp.com/icons/1040502664506646548/bb5a51c4659cf47bdd942bb11e974da7.webp?size=240"
|
||||
}
|
||||
alt="Server pfp"
|
||||
/>
|
||||
<h1>{guild()?.name ?? "li'l Judds home base"}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Guild</h2>
|
||||
<p>General settings for this guild.</p>
|
||||
<div class="flex-row">
|
||||
<label for="timezone">Timezone for your server:</label>
|
||||
<input
|
||||
type="text"
|
||||
list="timezones"
|
||||
id="timezone"
|
||||
ref={timezoneRef!}
|
||||
// disabled={!tzNames().find((e) => e === timezone())}
|
||||
onInput={(e) => setTimezone(e.target.value)}
|
||||
/>
|
||||
|
||||
<datalist id="timezones">
|
||||
<Index each={tzNames()}>
|
||||
{(zone) => <option value={zone()} />}
|
||||
</Index>
|
||||
</datalist>
|
||||
|
||||
<button
|
||||
disabled={guessTZ() === timezone()}
|
||||
title={"Detected: " + guessTZ()}
|
||||
onClick={() => setTimezone(guessTZ())}
|
||||
>
|
||||
Auto-detect
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Features</h2>
|
||||
<p>Configure the features of the bot</p>
|
||||
<label for="timePlanning" class="flex-row">
|
||||
<p>Time Planning </p>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
config.features.timePlanning.enabled ? faToggleOn : faToggleOff
|
||||
}
|
||||
size="xl"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
hidden
|
||||
type="checkbox"
|
||||
id="timePlanning"
|
||||
ref={timePlanningRef!}
|
||||
onInput={(e) =>
|
||||
setConfig("features", "timePlanning", "enabled", e.target.checked)
|
||||
}
|
||||
/>
|
||||
<div
|
||||
class="sub"
|
||||
classList={{ disabled: !config.features.timePlanning.enabled }}
|
||||
>
|
||||
<div class="flex-row">
|
||||
<label>Target channel:</label>
|
||||
<select value={timezone()}>
|
||||
<optgroup label="--Select a Channel--">
|
||||
<Index each={tzNames()}>
|
||||
{(channel) => <option>{channel()}</option>}
|
||||
</Index>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<label for="pingableRoles" class="flex-row">
|
||||
<p>Enable pingable Roles:</p>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
config.features.timePlanning.pingableRoles
|
||||
? faToggleOn
|
||||
: faToggleOff
|
||||
}
|
||||
size="xl"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
hidden
|
||||
type="checkbox"
|
||||
id="pingableRoles"
|
||||
ref={pingableRolesRef!}
|
||||
onInput={(e) =>
|
||||
setConfig(
|
||||
"features",
|
||||
"timePlanning",
|
||||
"pingableRoles",
|
||||
e.target.checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button>Apply</button>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default config;
|
124
src/routes/config/index.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
import { getSession } from "@auth/solid-start";
|
||||
import {
|
||||
faBadgeCheck,
|
||||
faCircleExclamation,
|
||||
faPlus,
|
||||
} from "@fortawesome/pro-regular-svg-icons";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { eq } from "drizzle-orm";
|
||||
import createClient from "openapi-fetch";
|
||||
import { For, createResource } from "solid-js";
|
||||
import { getRequestEvent } from "solid-js/web";
|
||||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||
import Layout from "~/components/Layout";
|
||||
import db from "~/drizzle";
|
||||
import { accounts } from "~/drizzle/schema";
|
||||
import { authOptions } from "~/server/auth";
|
||||
import { paths } from "~/types/discord";
|
||||
import "../../styles/pages/config.scss";
|
||||
|
||||
const initialValue = () => ({
|
||||
success: null as boolean | null,
|
||||
guilds: [] as {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string | null | undefined;
|
||||
}[],
|
||||
});
|
||||
|
||||
const getPayload = async (): Promise<
|
||||
| { success: false; message: string }
|
||||
| (ReturnType<typeof initialValue> & { success: true })
|
||||
> => {
|
||||
("use server");
|
||||
const event = getRequestEvent();
|
||||
if (!event) return { success: false, message: "No request event!" };
|
||||
|
||||
const session = await getSession(event.request, authOptions);
|
||||
if (!session?.user?.id)
|
||||
return { success: false, message: "No user with id!" };
|
||||
|
||||
const { DISCORD_ACCESS_TOKEN } = (
|
||||
await db
|
||||
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
|
||||
.from(accounts)
|
||||
.where(eq(accounts.userId, session.user?.id))
|
||||
.limit(1)
|
||||
.execute()
|
||||
)[0];
|
||||
if (!DISCORD_ACCESS_TOKEN)
|
||||
return { success: false, message: "No discord access token!" };
|
||||
|
||||
const { GET } = createClient<paths>({
|
||||
baseUrl: "https://discord.com/api/v10",
|
||||
});
|
||||
const { data: guilds, error } = await GET("/users/@me/guilds", {
|
||||
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
|
||||
});
|
||||
|
||||
console.log("guilds", guilds);
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
return { success: false, message: "Error on discord api request!" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
guilds:
|
||||
guilds
|
||||
?.filter((e) => parseInt(e.permissions) & (1 << 5))
|
||||
.map(({ id, name, icon }) => ({ id, name, icon })) ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
function index() {
|
||||
const navigator = useNavigate();
|
||||
|
||||
const [payload] = createResource(async () => {
|
||||
const payload = await getPayload();
|
||||
|
||||
if (!payload.success) {
|
||||
console.log(payload.message, "No success");
|
||||
navigator("/", { replace: false });
|
||||
return initialValue();
|
||||
}
|
||||
console.log("success");
|
||||
return payload;
|
||||
});
|
||||
|
||||
const icons = [faPlus, faCircleExclamation, faBadgeCheck];
|
||||
const colors = [undefined, "orange", "green"];
|
||||
|
||||
return (
|
||||
<Layout site="config">
|
||||
<h3 class="text-center">Configure li'l Judd in</h3>
|
||||
<div>
|
||||
<For each={payload()?.guilds ?? []}>
|
||||
{(guild, i) => (
|
||||
<a href={`/config/${guild.id}`} class="flex-row centered">
|
||||
<img
|
||||
class="guildpfp"
|
||||
src={
|
||||
guild.icon
|
||||
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=240`
|
||||
: "https://cdn.discordapp.com/icons/1040502664506646548/bb5a51c4659cf47bdd942bb11e974da7.webp?size=240"
|
||||
}
|
||||
alt="Server pfp"
|
||||
/>
|
||||
<h1>{guild.name}</h1>
|
||||
<FontAwesomeIcon
|
||||
// beat={i() % 3 === 1}
|
||||
color={colors[i() % 3]}
|
||||
icon={icons[i() % 3]}
|
||||
size="xl"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default index;
|