Generated initial files for project
This commit is contained in:
commit
f36d5cfce0
35
.devcontainer/devcontainer.json
Normal file
35
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||||
|
{
|
||||||
|
"name": "Python 3",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.isort",
|
||||||
|
"njpwerner.autodocstring",
|
||||||
|
"ms-python.black-formatter",
|
||||||
|
"ms-python.pylint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "pip3 install --user -r requirements.txt",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
358
.gitignore
vendored
Normal file
358
.gitignore
vendored
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,svelte,python,linux,node
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,svelte,python,linux,node
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
### Svelte ###
|
||||||
|
# gitignore template for the SvelteKit, frontend web component framework
|
||||||
|
# website: https://kit.svelte.dev/
|
||||||
|
|
||||||
|
.svelte-kit/
|
||||||
|
package
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,svelte,python,linux,node
|
||||||
|
|
||||||
|
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
||||||
|
|
24
mlstmyfasta.client/.gitignore
vendored
Normal file
24
mlstmyfasta.client/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
test-results
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
1
mlstmyfasta.client/.npmrc
Normal file
1
mlstmyfasta.client/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
4
mlstmyfasta.client/.prettierignore
Normal file
4
mlstmyfasta.client/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
15
mlstmyfasta.client/.prettierrc
Normal file
15
mlstmyfasta.client/.prettierrc
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
mlstmyfasta.client/README.md
Normal file
38
mlstmyfasta.client/README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npx sv create
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
6
mlstmyfasta.client/e2e/demo.test.js
Normal file
6
mlstmyfasta.client/e2e/demo.test.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test('home page has expected h1', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.locator('h1')).toBeVisible();
|
||||||
|
});
|
24
mlstmyfasta.client/eslint.config.js
Normal file
24
mlstmyfasta.client/eslint.config.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import globals from 'globals';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||||
|
|
||||||
|
/** @type {import('eslint').Linter.Config[]} */
|
||||||
|
export default [
|
||||||
|
includeIgnoreFile(gitignorePath),
|
||||||
|
js.configs.recommended,
|
||||||
|
...svelte.configs['flat/recommended'],
|
||||||
|
prettier,
|
||||||
|
...svelte.configs['flat/prettier'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
19
mlstmyfasta.client/jsconfig.json
Normal file
19
mlstmyfasta.client/jsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
4694
mlstmyfasta.client/package-lock.json
generated
Normal file
4694
mlstmyfasta.client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
mlstmyfasta.client/package.json
Normal file
45
mlstmyfasta.client/package.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "mlstmyfasta.client",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"test:unit": "vitest",
|
||||||
|
"test": "npm run test:unit -- --run && npm run test:e2e",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.2.3",
|
||||||
|
"@fontsource/fira-mono": "^5.0.0",
|
||||||
|
"@neoconfetti/svelte": "^2.0.0",
|
||||||
|
"@playwright/test": "^1.45.3",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.7.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-svelte": "^2.36.0",
|
||||||
|
"globals": "^15.0.0",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwindcss": "^3.4.9",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^5.4.11",
|
||||||
|
"vitest": "^2.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
|
"@tailwindcss/typography": "^0.5.15"
|
||||||
|
}
|
||||||
|
}
|
10
mlstmyfasta.client/playwright.config.js
Normal file
10
mlstmyfasta.client/playwright.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run build && npm run preview',
|
||||||
|
port: 4173
|
||||||
|
},
|
||||||
|
|
||||||
|
testDir: 'e2e'
|
||||||
|
});
|
6
mlstmyfasta.client/postcss.config.js
Normal file
6
mlstmyfasta.client/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
110
mlstmyfasta.client/src/app.css
Normal file
110
mlstmyfasta.client/src/app.css
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
|
@import '@fontsource/fira-mono';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
--font-mono: 'Fira Mono', monospace;
|
||||||
|
--color-bg-0: rgb(202, 216, 228);
|
||||||
|
--color-bg-1: hsl(209, 36%, 86%);
|
||||||
|
--color-bg-2: hsl(224, 44%, 95%);
|
||||||
|
--color-theme-1: #ff3e00;
|
||||||
|
--color-theme-2: #4075a6;
|
||||||
|
--color-text: rgba(0, 0, 0, 0.7);
|
||||||
|
--column-width: 42rem;
|
||||||
|
--column-margin-top: 4rem;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-color: var(--color-bg-1);
|
||||||
|
background-size: 100vw 100vh;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
50% 50% at 50% 50%,
|
||||||
|
rgba(255, 255, 255, 0.75) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
),
|
||||||
|
linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-theme-1);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
background-color: rgba(255, 255, 255, 0.45);
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
|
||||||
|
padding: 0.5em;
|
||||||
|
overflow-x: auto;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-column {
|
||||||
|
display: flex;
|
||||||
|
max-width: 48rem;
|
||||||
|
flex: 0.6;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
13
mlstmyfasta.client/src/app.d.ts
vendored
Normal file
13
mlstmyfasta.client/src/app.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
12
mlstmyfasta.client/src/app.html
Normal file
12
mlstmyfasta.client/src/app.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
mlstmyfasta.client/src/demo.spec.js
Normal file
7
mlstmyfasta.client/src/demo.spec.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('sum test', () => {
|
||||||
|
it('adds 1 + 2 to equal 3', () => {
|
||||||
|
expect(1 + 2).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
58
mlstmyfasta.client/src/routes/+layout.svelte
Normal file
58
mlstmyfasta.client/src/routes/+layout.svelte
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script>
|
||||||
|
import Header from './Header.svelte';
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
|
/** @type {{children: import('svelte').Snippet}} */
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="app">
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{@render children()}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to learn about SvelteKit
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 64rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 480px) {
|
||||||
|
footer {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
3
mlstmyfasta.client/src/routes/+page.js
Normal file
3
mlstmyfasta.client/src/routes/+page.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// since there's no dynamic data here, we can prerender
|
||||||
|
// it so that it gets served as a static asset in production
|
||||||
|
export const prerender = true;
|
59
mlstmyfasta.client/src/routes/+page.svelte
Normal file
59
mlstmyfasta.client/src/routes/+page.svelte
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script>
|
||||||
|
import Counter from './Counter.svelte';
|
||||||
|
import welcome from '$lib/images/svelte-welcome.webp';
|
||||||
|
import welcomeFallback from '$lib/images/svelte-welcome.png';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Home</title>
|
||||||
|
<meta name="description" content="Svelte demo app" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h1>
|
||||||
|
<span class="welcome">
|
||||||
|
<picture>
|
||||||
|
<source srcset={welcome} type="image/webp" />
|
||||||
|
<img src={welcomeFallback} alt="Welcome" />
|
||||||
|
</picture>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
to your new<br />SvelteKit app
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
try editing <strong>src/routes/+page.svelte</strong>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Counter />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
padding: 0 0 calc(100% * 495 / 2048) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome img {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
110
mlstmyfasta.client/src/routes/Counter.svelte
Normal file
110
mlstmyfasta.client/src/routes/Counter.svelte
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script>
|
||||||
|
import { spring } from 'svelte/motion';
|
||||||
|
|
||||||
|
let count = $state(0);
|
||||||
|
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
const displayedCount = spring(count);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
displayedCount.set(count);
|
||||||
|
});
|
||||||
|
let offset = $derived(modulo($displayedCount, 1));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} n
|
||||||
|
* @param {number} m
|
||||||
|
*/
|
||||||
|
function modulo(n, m) {
|
||||||
|
// handle negative numbers
|
||||||
|
return ((n % m) + m) % m;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="counter">
|
||||||
|
<button onclick={() => (count -= 1)} aria-label="Decrease the counter by one">
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 1 1">
|
||||||
|
<path d="M0,0.5 L1,0.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="counter-viewport">
|
||||||
|
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
|
||||||
|
<strong class="hidden" aria-hidden="true">{Math.floor($displayedCount + 1)}</strong>
|
||||||
|
<strong>{Math.floor($displayedCount)}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick={() => (count += 1)} aria-label="Increase the counter by one">
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 1 1">
|
||||||
|
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.counter {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter button {
|
||||||
|
width: 2em;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
touch-action: manipulation;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter button:hover {
|
||||||
|
background-color: var(--color-bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 25%;
|
||||||
|
height: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-viewport {
|
||||||
|
width: 8em;
|
||||||
|
height: 4em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-viewport strong {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--color-theme-1);
|
||||||
|
font-size: 4rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-digits {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
top: -100%;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
129
mlstmyfasta.client/src/routes/Header.svelte
Normal file
129
mlstmyfasta.client/src/routes/Header.svelte
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import logo from '$lib/images/svelte-logo.svg';
|
||||||
|
import github from '$lib/images/github.svg';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="corner">
|
||||||
|
<a href="https://svelte.dev/docs/kit">
|
||||||
|
<img src={logo} alt="SvelteKit" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<svg viewBox="0 0 2 3" aria-hidden="true">
|
||||||
|
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
|
||||||
|
</svg>
|
||||||
|
<ul>
|
||||||
|
<li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li aria-current={$page.url.pathname === '/about' ? 'page' : undefined}>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
</li>
|
||||||
|
<li aria-current={$page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
|
||||||
|
<a href="/sverdle">Sverdle</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<svg viewBox="0 0 2 3" aria-hidden="true">
|
||||||
|
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
|
||||||
|
</svg>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="corner">
|
||||||
|
<a href="https://github.com/sveltejs/kit">
|
||||||
|
<img src={github} alt="GitHub" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner {
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner img {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
--background: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 2em;
|
||||||
|
height: 3em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 3em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
background: var(--background);
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
li[aria-current='page']::before {
|
||||||
|
--size: 6px;
|
||||||
|
content: '';
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: calc(50% - var(--size));
|
||||||
|
border: var(--size) solid transparent;
|
||||||
|
border-top: var(--size) solid var(--color-theme-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-theme-1);
|
||||||
|
}
|
||||||
|
</style>
|
9
mlstmyfasta.client/src/routes/about/+page.js
Normal file
9
mlstmyfasta.client/src/routes/about/+page.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
// we don't need any JS on this page, though we'll load
|
||||||
|
// it in dev so that we get hot module replacement
|
||||||
|
export const csr = dev;
|
||||||
|
|
||||||
|
// since there's no dynamic data here, we can prerender
|
||||||
|
// it so that it gets served as a static asset in production
|
||||||
|
export const prerender = true;
|
26
mlstmyfasta.client/src/routes/about/+page.svelte
Normal file
26
mlstmyfasta.client/src/routes/about/+page.svelte
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<svelte:head>
|
||||||
|
<title>About</title>
|
||||||
|
<meta name="description" content="About this app" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="text-column">
|
||||||
|
<h1>About this app</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is a <a href="https://svelte.dev/docs/kit">SvelteKit</a> app. You can make your own by typing
|
||||||
|
the following into your command line and following the prompts:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>npx sv create</pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The page you're looking at is purely static HTML, with no client-side interactivity needed.
|
||||||
|
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
|
||||||
|
the devtools network panel and reloading.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The <a href="/sverdle">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try
|
||||||
|
using it with JavaScript disabled!
|
||||||
|
</p>
|
||||||
|
</div>
|
70
mlstmyfasta.client/src/routes/sverdle/+page.server.js
Normal file
70
mlstmyfasta.client/src/routes/sverdle/+page.server.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
import { Game } from './game';
|
||||||
|
|
||||||
|
/** @satisfies {import('./$types').PageServerLoad} */
|
||||||
|
export const load = ({ cookies }) => {
|
||||||
|
const game = new Game(cookies.get('sverdle'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* The player's guessed words so far
|
||||||
|
*/
|
||||||
|
guesses: game.guesses,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of strings like '__x_c' corresponding to the guesses, where 'x' means
|
||||||
|
* an exact match, and 'c' means a close match (right letter, wrong place)
|
||||||
|
*/
|
||||||
|
answers: game.answers,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The correct answer, revealed if the game is over
|
||||||
|
*/
|
||||||
|
answer: game.answers.length >= 6 ? game.answer : null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @satisfies {import('./$types').Actions} */
|
||||||
|
export const actions = {
|
||||||
|
/**
|
||||||
|
* Modify game state in reaction to a keypress. If client-side JavaScript
|
||||||
|
* is available, this will happen in the browser instead of here
|
||||||
|
*/
|
||||||
|
update: async ({ request, cookies }) => {
|
||||||
|
const game = new Game(cookies.get('sverdle'));
|
||||||
|
|
||||||
|
const data = await request.formData();
|
||||||
|
const key = data.get('key');
|
||||||
|
|
||||||
|
const i = game.answers.length;
|
||||||
|
|
||||||
|
if (key === 'backspace') {
|
||||||
|
game.guesses[i] = game.guesses[i].slice(0, -1);
|
||||||
|
} else {
|
||||||
|
game.guesses[i] += key;
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies.set('sverdle', game.toString(), { path: '/' });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify game state in reaction to a guessed word. This logic always runs on
|
||||||
|
* the server, so that people can't cheat by peeking at the JavaScript
|
||||||
|
*/
|
||||||
|
enter: async ({ request, cookies }) => {
|
||||||
|
const game = new Game(cookies.get('sverdle'));
|
||||||
|
|
||||||
|
const data = await request.formData();
|
||||||
|
const guess = /** @type {string[]} */ (data.getAll('guess'));
|
||||||
|
|
||||||
|
if (!game.enter(guess)) {
|
||||||
|
return fail(400, { badGuess: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies.set('sverdle', game.toString(), { path: '/' });
|
||||||
|
},
|
||||||
|
|
||||||
|
restart: async ({ cookies }) => {
|
||||||
|
cookies.delete('sverdle', { path: '/' });
|
||||||
|
}
|
||||||
|
};
|
420
mlstmyfasta.client/src/routes/sverdle/+page.svelte
Normal file
420
mlstmyfasta.client/src/routes/sverdle/+page.svelte
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
<script>
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import { confetti } from '@neoconfetti/svelte';
|
||||||
|
|
||||||
|
import { reducedMotion } from './reduced-motion';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Props
|
||||||
|
* @property {import('./$types').PageData} data
|
||||||
|
* @property {import('./$types').ActionData} form
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Props}
|
||||||
|
*/
|
||||||
|
let { data, form = $bindable() } = $props();
|
||||||
|
|
||||||
|
/** Whether or not the user has won */
|
||||||
|
let won = $derived(data.answers.at(-1) === 'xxxxx');
|
||||||
|
|
||||||
|
/** The index of the current guess */
|
||||||
|
let i = $derived(won ? -1 : data.answers.length);
|
||||||
|
|
||||||
|
/** The current guess */
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let currentGuess = $state(data.guesses[i] || '');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
currentGuess = data.guesses[i] || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Whether the current guess can be submitted */
|
||||||
|
let submittable = $derived(currentGuess.length === 5);
|
||||||
|
|
||||||
|
const { classnames, description } = $derived.by(() => {
|
||||||
|
/**
|
||||||
|
* A map of classnames for all letters that have been guessed,
|
||||||
|
* used for styling the keyboard
|
||||||
|
* @type {Record<string, 'exact' | 'close' | 'missing'>}
|
||||||
|
*/
|
||||||
|
let classnames = {};
|
||||||
|
/**
|
||||||
|
* A map of descriptions for all letters that have been guessed,
|
||||||
|
* used for adding text for assistive technology (e.g. screen readers)
|
||||||
|
* @type {Record<string, string>}
|
||||||
|
*/
|
||||||
|
let description = {};
|
||||||
|
data.answers.forEach((answer, i) => {
|
||||||
|
const guess = data.guesses[i];
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
const letter = guess[i];
|
||||||
|
if (answer[i] === 'x') {
|
||||||
|
classnames[letter] = 'exact';
|
||||||
|
description[letter] = 'correct';
|
||||||
|
} else if (!classnames[letter]) {
|
||||||
|
classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
|
||||||
|
description[letter] = answer[i] === 'c' ? 'present' : 'absent';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { classnames, description };
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify the game state without making a trip to the server,
|
||||||
|
* if client-side JavaScript is enabled
|
||||||
|
* @param {MouseEvent} event
|
||||||
|
*/
|
||||||
|
function update(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');
|
||||||
|
|
||||||
|
if (key === 'backspace') {
|
||||||
|
currentGuess = currentGuess.slice(0, -1);
|
||||||
|
if (form?.badGuess) form.badGuess = false;
|
||||||
|
} else if (currentGuess.length < 5) {
|
||||||
|
currentGuess += key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger form logic in response to a keydown event, so that
|
||||||
|
* desktop users can use the keyboard to play the game
|
||||||
|
* @param {KeyboardEvent} event
|
||||||
|
*/
|
||||||
|
function keydown(event) {
|
||||||
|
if (event.metaKey) return;
|
||||||
|
|
||||||
|
if (event.key === 'Enter' && !submittable) return;
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector(`[data-key="${event.key}" i]`)
|
||||||
|
?.dispatchEvent(new MouseEvent('click', { cancelable: true }));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={keydown} />
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Sverdle</title>
|
||||||
|
<meta name="description" content="A Wordle clone written in SvelteKit" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1 class="visually-hidden">Sverdle</h1>
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="post"
|
||||||
|
action="?/enter"
|
||||||
|
use:enhance={() => {
|
||||||
|
// prevent default callback from resetting the form
|
||||||
|
return ({ update }) => {
|
||||||
|
update({ reset: false });
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a class="how-to-play" href="/sverdle/how-to-play">How to play</a>
|
||||||
|
|
||||||
|
<div class="grid" class:playing={!won} class:bad-guess={form?.badGuess}>
|
||||||
|
{#each Array.from(Array(6).keys()) as row (row)}
|
||||||
|
{@const current = row === i}
|
||||||
|
<h2 class="visually-hidden">Row {row + 1}</h2>
|
||||||
|
<div class="row" class:current>
|
||||||
|
{#each Array.from(Array(5).keys()) as column (column)}
|
||||||
|
{@const guess = current ? currentGuess : data.guesses[row]}
|
||||||
|
{@const answer = data.answers[row]?.[column]}
|
||||||
|
{@const value = guess?.[column] ?? ''}
|
||||||
|
{@const selected = current && column === guess.length}
|
||||||
|
{@const exact = answer === 'x'}
|
||||||
|
{@const close = answer === 'c'}
|
||||||
|
{@const missing = answer === '_'}
|
||||||
|
<div class="letter" class:exact class:close class:missing class:selected>
|
||||||
|
{value}
|
||||||
|
<span class="visually-hidden">
|
||||||
|
{#if exact}
|
||||||
|
(correct)
|
||||||
|
{:else if close}
|
||||||
|
(present)
|
||||||
|
{:else if missing}
|
||||||
|
(absent)
|
||||||
|
{:else}
|
||||||
|
empty
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<input name="guess" disabled={!current} type="hidden" {value} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
{#if won || data.answers.length >= 6}
|
||||||
|
{#if !won && data.answer}
|
||||||
|
<p>the answer was "{data.answer}"</p>
|
||||||
|
{/if}
|
||||||
|
<button data-key="enter" class="restart selected" formaction="?/restart">
|
||||||
|
{won ? 'you won :)' : `game over :(`} play again?
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="keyboard">
|
||||||
|
<button data-key="enter" class:selected={submittable} disabled={!submittable}>enter</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={update}
|
||||||
|
data-key="backspace"
|
||||||
|
formaction="?/update"
|
||||||
|
name="key"
|
||||||
|
value="backspace"
|
||||||
|
>
|
||||||
|
back
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row}
|
||||||
|
<div class="row">
|
||||||
|
{#each row as letter}
|
||||||
|
<button
|
||||||
|
onclick={update}
|
||||||
|
data-key={letter}
|
||||||
|
class={classnames[letter]}
|
||||||
|
disabled={submittable}
|
||||||
|
formaction="?/update"
|
||||||
|
name="key"
|
||||||
|
value={letter}
|
||||||
|
aria-label="{letter} {description[letter] || ''}"
|
||||||
|
>
|
||||||
|
{letter}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if won}
|
||||||
|
<div
|
||||||
|
style="position: absolute; left: 50%; top: 30%"
|
||||||
|
use:confetti={{
|
||||||
|
particleCount: $reducedMotion ? 0 : undefined,
|
||||||
|
force: 0.7,
|
||||||
|
stageWidth: window.innerWidth,
|
||||||
|
stageHeight: window.innerHeight,
|
||||||
|
colors: ['#ff3e00', '#40b3ff', '#676778']
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.how-to-play {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.how-to-play::before {
|
||||||
|
content: 'i';
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 900;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
padding: 0.2em;
|
||||||
|
line-height: 1;
|
||||||
|
border: 1.5px solid var(--color-text);
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0.5em 0 0;
|
||||||
|
position: relative;
|
||||||
|
top: -0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
--width: min(100vw, 40vh, 380px);
|
||||||
|
max-width: var(--width);
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid .row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
grid-gap: 0.2rem;
|
||||||
|
margin: 0 0 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.grid.bad-guess .row.current {
|
||||||
|
animation: wiggle 0.5s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid.playing .row.current {
|
||||||
|
filter: drop-shadow(3px 3px 10px var(--color-bg-0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-transform: lowercase;
|
||||||
|
border: none;
|
||||||
|
font-size: calc(0.08 * var(--width));
|
||||||
|
border-radius: 2px;
|
||||||
|
background: white;
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter.missing {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter.exact {
|
||||||
|
background: var(--color-theme-2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter.close {
|
||||||
|
border: 2px solid var(--color-theme-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
outline: 2px solid var(--color-theme-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: min(18vh, 10rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard {
|
||||||
|
--gap: 0.2rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gap);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard .row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button,
|
||||||
|
.keyboard button:disabled {
|
||||||
|
--size: min(8vw, 4vh, 40px);
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
width: var(--size);
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: calc(var(--size) * 0.5);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button.exact {
|
||||||
|
background: var(--color-theme-2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button.missing {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button.close {
|
||||||
|
border: 2px solid var(--color-theme-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button:focus {
|
||||||
|
background: var(--color-theme-1);
|
||||||
|
color: white;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button[data-key='enter'],
|
||||||
|
.keyboard button[data-key='backspace'] {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: calc(1.5 * var(--size));
|
||||||
|
height: calc(1 / 3 * (100% - 2 * var(--gap)));
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: calc(0.3 * var(--size));
|
||||||
|
padding-top: calc(0.15 * var(--size));
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button[data-key='enter'] {
|
||||||
|
right: calc(50% + 3.5 * var(--size) + 0.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button[data-key='backspace'] {
|
||||||
|
left: calc(50% + 3.5 * var(--size) + 0.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard button[data-key='enter']:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restart {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 2px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restart:focus,
|
||||||
|
.restart:hover {
|
||||||
|
background: var(--color-theme-1);
|
||||||
|
color: white;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-6px);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: translateX(+4px);
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
72
mlstmyfasta.client/src/routes/sverdle/game.js
Normal file
72
mlstmyfasta.client/src/routes/sverdle/game.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { words, allowed } from './words.server';
|
||||||
|
|
||||||
|
export class Game {
|
||||||
|
/**
|
||||||
|
* Create a game object from the player's cookie, or initialise a new game
|
||||||
|
* @param {string | undefined} serialized
|
||||||
|
*/
|
||||||
|
constructor(serialized = undefined) {
|
||||||
|
if (serialized) {
|
||||||
|
const [index, guesses, answers] = serialized.split('-');
|
||||||
|
|
||||||
|
this.index = +index;
|
||||||
|
this.guesses = guesses ? guesses.split(' ') : [];
|
||||||
|
this.answers = answers ? answers.split(' ') : [];
|
||||||
|
} else {
|
||||||
|
this.index = Math.floor(Math.random() * words.length);
|
||||||
|
this.guesses = ['', '', '', '', '', ''];
|
||||||
|
this.answers = /** @type {string[]} */ ([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.answer = words[this.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update game state based on a guess of a five-letter word. Returns
|
||||||
|
* true if the guess was valid, false otherwise
|
||||||
|
* @param {string[]} letters
|
||||||
|
*/
|
||||||
|
enter(letters) {
|
||||||
|
const word = letters.join('');
|
||||||
|
const valid = allowed.has(word);
|
||||||
|
|
||||||
|
if (!valid) return false;
|
||||||
|
|
||||||
|
this.guesses[this.answers.length] = word;
|
||||||
|
|
||||||
|
const available = Array.from(this.answer);
|
||||||
|
const answer = Array(5).fill('_');
|
||||||
|
|
||||||
|
// first, find exact matches
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
if (letters[i] === available[i]) {
|
||||||
|
answer[i] = 'x';
|
||||||
|
available[i] = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// then find close matches (this has to happen
|
||||||
|
// in a second step, otherwise an early close
|
||||||
|
// match can prevent a later exact match)
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
if (answer[i] === '_') {
|
||||||
|
const index = available.indexOf(letters[i]);
|
||||||
|
if (index !== -1) {
|
||||||
|
answer[i] = 'c';
|
||||||
|
available[index] = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.answers.push(answer.join(''));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize game state so it can be set as a cookie
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
return `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
// we don't need any JS on this page, though we'll load
|
||||||
|
// it in dev so that we get hot module replacement
|
||||||
|
export const csr = dev;
|
||||||
|
|
||||||
|
// since there's no dynamic data here, we can prerender
|
||||||
|
// it so that it gets served as a static asset in production
|
||||||
|
export const prerender = true;
|
@ -0,0 +1,95 @@
|
|||||||
|
<svelte:head>
|
||||||
|
<title>How to play Sverdle</title>
|
||||||
|
<meta name="description" content="How to play Sverdle" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="text-column">
|
||||||
|
<h1>How to play Sverdle</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Sverdle is a clone of <a href="https://www.nytimes.com/games/wordle/index.html">Wordle</a>, the
|
||||||
|
word guessing game. To play, enter a five-letter English word. For example:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<span class="close">r</span>
|
||||||
|
<span class="missing">i</span>
|
||||||
|
<span class="close">t</span>
|
||||||
|
<span class="missing">z</span>
|
||||||
|
<span class="exact">y</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The <span class="exact">y</span> is in the right place. <span class="close">r</span> and
|
||||||
|
<span class="close">t</span>
|
||||||
|
are the right letters, but in the wrong place. The other letters are wrong, and can be discarded.
|
||||||
|
Let's make another guess:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<span class="exact">p</span>
|
||||||
|
<span class="exact">a</span>
|
||||||
|
<span class="exact">r</span>
|
||||||
|
<span class="exact">t</span>
|
||||||
|
<span class="exact">y</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>This time we guessed right! You have <strong>six</strong> guesses to get the word.</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it
|
||||||
|
impossible to cheat. It uses <code><form></code> and cookies to submit data, meaning you can
|
||||||
|
even play with JavaScript disabled!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
width: 2.4em;
|
||||||
|
height: 2.4em;
|
||||||
|
background-color: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-width: 2px;
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--color-theme-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exact {
|
||||||
|
background: var(--color-theme-2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin: 1rem 0;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example span {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p span {
|
||||||
|
position: relative;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 1px;
|
||||||
|
font-size: 0.4em;
|
||||||
|
transform: scale(2) translate(0, -10%);
|
||||||
|
margin: 0 1em;
|
||||||
|
}
|
||||||
|
</style>
|
26
mlstmyfasta.client/src/routes/sverdle/reduced-motion.js
Normal file
26
mlstmyfasta.client/src/routes/sverdle/reduced-motion.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { readable } from 'svelte/store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
const reducedMotionQuery = '(prefers-reduced-motion: reduce)';
|
||||||
|
|
||||||
|
const getInitialMotionPreference = () => {
|
||||||
|
if (!browser) return false;
|
||||||
|
return window.matchMedia(reducedMotionQuery).matches;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducedMotion = readable(getInitialMotionPreference(), (set) => {
|
||||||
|
if (browser) {
|
||||||
|
/**
|
||||||
|
* @param {MediaQueryListEvent} event
|
||||||
|
*/
|
||||||
|
const setReducedMotion = (event) => {
|
||||||
|
set(event.matches);
|
||||||
|
};
|
||||||
|
const mediaQueryList = window.matchMedia(reducedMotionQuery);
|
||||||
|
mediaQueryList.addEventListener('change', setReducedMotion);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQueryList.removeEventListener('change', setReducedMotion);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
12980
mlstmyfasta.client/src/routes/sverdle/words.server.js
Normal file
12980
mlstmyfasta.client/src/routes/sverdle/words.server.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
mlstmyfasta.client/static/favicon.png
Normal file
BIN
mlstmyfasta.client/static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
3
mlstmyfasta.client/static/robots.txt
Normal file
3
mlstmyfasta.client/static/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
13
mlstmyfasta.client/svelte.config.js
Normal file
13
mlstmyfasta.client/svelte.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
13
mlstmyfasta.client/tailwind.config.js
Normal file
13
mlstmyfasta.client/tailwind.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import forms from '@tailwindcss/forms';
|
||||||
|
import typography from '@tailwindcss/typography';
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [typography, forms]
|
||||||
|
};
|
10
mlstmyfasta.client/vite.config.js
Normal file
10
mlstmyfasta.client/vite.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||||
|
}
|
||||||
|
});
|
31
mlstmyfasta.server/src/mlstmyfasta/setup.cfg
Normal file
31
mlstmyfasta.server/src/mlstmyfasta/setup.cfg
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
[metadata]
|
||||||
|
name = mlstmyfasta.server
|
||||||
|
|
||||||
|
[options]
|
||||||
|
package_dir =
|
||||||
|
= src
|
||||||
|
install_requires =
|
||||||
|
sqlalchemy ==2.0
|
||||||
|
flask-sqlalchemy ==3.0
|
||||||
|
flask ==2.3
|
||||||
|
pyvcf3 ==1
|
||||||
|
requests ==2.29
|
||||||
|
cachier ==2.1
|
||||||
|
biopython ==1.81
|
||||||
|
scipy ==1.10
|
||||||
|
spacy ==3.4
|
||||||
|
matplotlib ==3.7
|
||||||
|
cupy ==12.1
|
||||||
|
en-core-sci-lg ==0.5.1
|
||||||
|
scispacy ==0.5.2
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
pythonpath = src
|
||||||
|
testpaths = tests
|
||||||
|
log_cli = 1
|
||||||
|
|
||||||
|
[pylint]
|
||||||
|
max-line-length = 88
|
||||||
|
|
||||||
|
[isort]
|
||||||
|
profile = black
|
Loading…
x
Reference in New Issue
Block a user