This commit is contained in:
2026-02-21 08:05:33 -05:00
parent bba007f1dc
commit 9531953b48
114 changed files with 872 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# top level
.vscode
.DS_Store
.git
# shared backend and frontend
*/.DS_Store
*/node_modules
*/package-lock.json
*/.env
# backend
backend/data/anki/decks/*

43
backend/app.js Normal file
View File

@@ -0,0 +1,43 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var photosearch = require('./routes/photosearch');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/search/photos', photosearch)
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

90
backend/bin/www Executable file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('backend:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

89
backend/lib/anki.js Normal file
View File

@@ -0,0 +1,89 @@
import { readFile } from 'fs/promises'
import { open } from 'sqlite'
import sqlite3 from 'sqlite3'
import AdmZip from 'adm-zip'
// database structure: https://github.com/ankidroid/Anki-Android/wiki/Database-Structure
// python example: https://github.com/patarapolw/ankisync2
// old js example: https://github.com/nornagon/mkanki/blob/master/index.js
// to create a zip file
// const zip = new AdmZip()
// zip.addLocalFolder('./folder_to_zip')
// zip.writeZip('./output.zip')
class Deck {
/**
*
* @param info ID of internal deck, or filename or anki export
* @param process whether or not this is an external anki export to process
*/
constructor(info, process) {
// pull out variables
this.notes = null // this is what is edited; may create multiple cards
this.media = null // image and audio files attached to cards/notes
}
/**
* Loads an internal deck
*/
async load(id) {
// open database
// open media files
}
/**
* Processes an anki export for internal use. Currently only supports
* .apkg files. In the future, may support:
* colpkg
* txt (notes)
* txt (cards)
*
* @param filename path of file to process
*/
async process(filename) {
return await this.processApkg(filename)
}
/**
* Processes a .apkg file into a database
*
* @param filename name of file to open and process
*/
async processApkg(filename) {
// error handling
if (!filename) throw new Error('Must specify a .apkg filename')
// setup
let uuid = crypto.randomUUID()
let location = `../data/anki/decks/${uuid}/`
// make the resource folder if it does not exist already
// let resource = ''
// if (!fs.existsSync(resource)) fs.mkdirSync(resource, { recursive: true })
// extract zip
// https://stuk.github.io/jszip/documentation/howto/read_zip.html#in-nodejs
let zip = new AdmZip(filename)
zip.extractAllTo(location, true)
// extract the notes database
let db = await open({
filename: location + 'collection.anki2',
driver: sqlite3.cached.Database
})
let result = await db.get('SELECT sfld FROM notes WHERE id = :n', 1486785585103)
console.log(result);
// extract media files
// ? process/rename media files and update in db?
// load data into this obj
// return { notes, media }
}
}
export default Deck

BIN
backend/lib/test.apkg Normal file

Binary file not shown.

4
backend/lib/test.js Normal file
View File

@@ -0,0 +1,4 @@
import Deck from './anki.js'
let deck = new Deck()
await deck.process('test.apkg');

BIN
backend/lib/test/0 Normal file

Binary file not shown.

BIN
backend/lib/test/1 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
backend/lib/test/10 Normal file

Binary file not shown.

BIN
backend/lib/test/11 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
backend/lib/test/12 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
backend/lib/test/13 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
backend/lib/test/14 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
backend/lib/test/15 Normal file

Binary file not shown.

BIN
backend/lib/test/16 Normal file

Binary file not shown.

BIN
backend/lib/test/17 Normal file

Binary file not shown.

BIN
backend/lib/test/18 Normal file

Binary file not shown.

BIN
backend/lib/test/19 Normal file

Binary file not shown.

BIN
backend/lib/test/2 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
backend/lib/test/20 Normal file

Binary file not shown.

BIN
backend/lib/test/21 Normal file

Binary file not shown.

BIN
backend/lib/test/22 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
backend/lib/test/23 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
backend/lib/test/24 Normal file

Binary file not shown.

BIN
backend/lib/test/25 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
backend/lib/test/26 Normal file

Binary file not shown.

BIN
backend/lib/test/27 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
backend/lib/test/28 Normal file

Binary file not shown.

BIN
backend/lib/test/29 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
backend/lib/test/3 Normal file

Binary file not shown.

BIN
backend/lib/test/30 Normal file

Binary file not shown.

BIN
backend/lib/test/31 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
backend/lib/test/32 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
backend/lib/test/33 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
backend/lib/test/34 Normal file

Binary file not shown.

BIN
backend/lib/test/35 Normal file

Binary file not shown.

BIN
backend/lib/test/36 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
backend/lib/test/37 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
backend/lib/test/38 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
backend/lib/test/39 Normal file

Binary file not shown.

BIN
backend/lib/test/4 Normal file

Binary file not shown.

BIN
backend/lib/test/40 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
backend/lib/test/41 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
backend/lib/test/42 Normal file

Binary file not shown.

BIN
backend/lib/test/43 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
backend/lib/test/44 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
backend/lib/test/45 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
backend/lib/test/46 Normal file

Binary file not shown.

BIN
backend/lib/test/47 Normal file

Binary file not shown.

BIN
backend/lib/test/48 Normal file

Binary file not shown.

BIN
backend/lib/test/49 Normal file

Binary file not shown.

BIN
backend/lib/test/5 Normal file

Binary file not shown.

BIN
backend/lib/test/50 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
backend/lib/test/51 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
backend/lib/test/52 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
backend/lib/test/53 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
backend/lib/test/54 Normal file

Binary file not shown.

BIN
backend/lib/test/55 Normal file

Binary file not shown.

BIN
backend/lib/test/56 Normal file

Binary file not shown.

BIN
backend/lib/test/57 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
backend/lib/test/58 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
backend/lib/test/59 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
backend/lib/test/6 Normal file

Binary file not shown.

BIN
backend/lib/test/60 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
backend/lib/test/61 Normal file

Binary file not shown.

BIN
backend/lib/test/62 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
backend/lib/test/63 Normal file

Binary file not shown.

BIN
backend/lib/test/64 Normal file

Binary file not shown.

BIN
backend/lib/test/65 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
backend/lib/test/66 Normal file

Binary file not shown.

BIN
backend/lib/test/67 Normal file

Binary file not shown.

BIN
backend/lib/test/68 Normal file

Binary file not shown.

BIN
backend/lib/test/69 Normal file

Binary file not shown.

BIN
backend/lib/test/7 Normal file

Binary file not shown.

BIN
backend/lib/test/70 Normal file

Binary file not shown.

BIN
backend/lib/test/71 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
backend/lib/test/72 Normal file

Binary file not shown.

BIN
backend/lib/test/73 Normal file

Binary file not shown.

BIN
backend/lib/test/74 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
backend/lib/test/75 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
backend/lib/test/8 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
backend/lib/test/9 Normal file

Binary file not shown.

Binary file not shown.

1
backend/lib/test/media Normal file
View File

@@ -0,0 +1 @@
{"66": "rec1486791173.mp3", "12": "paste-5909874999552.jpg", "49": "rec1486790484.mp3", "54": "rec1486789453.mp3", "59": "paste-6051608920308.jpg", "75": "paste-7176890351876.jpg", "50": "paste-5952824672506.jpg", "11": "paste-5342939316470.jpg", "3": "rec1486790515.mp3", "73": "rec1486791444.mp3", "58": "paste-6743098654966.jpg", "26": "rec1486791092.mp3", "60": "paste-5596342386925.jpg", "68": "rec1486787151.mp3", "69": "rec1486790743.mp3", "0": "rec1486791250.mp3", "8": "paste-6219112644848.jpg", "18": "rec1486790648.mp3", "57": "paste-6880537608452.jpg", "61": "rec1486789557.mp3", "33": "paste-6343666696438.jpg", "7": "rec1486791037.mp3", "21": "rec1486786900.mp3", "44": "paste-5476083302645.jpg", "17": "rec1486790321.mp3", "9": "rec1486790979.mp3", "55": "rec1486790707.mp3", "64": "rec1486790888.mp3", "71": "paste-7563437408511.jpg", "53": "paste-6532645257453.jpg", "36": "paste-6322191859961.jpg", "31": "paste-7687991460090.jpg", "28": "rec1486790193.mp3", "47": "rec1486791121.mp3", "39": "rec1486791213.mp3", "14": "paste-7022271529203.jpg", "37": "paste-7288559501563.jpg", "15": "rec1486789738.mp3", "51": "paste-2929167696123.jpg", "25": "paste-7597797146867.jpg", "20": "rec1486790550.mp3", "19": "rec1486790446.mp3", "48": "rec1486788047.mp3", "74": "paste-5849745457397.jpg", "30": "rec1486791007.mp3", "10": "rec1486788946.mp3", "29": "paste-6846177870075.jpg", "4": "rec1486790287.mp3", "56": "rec1486789680.mp3", "62": "paste-5755256176881.jpg", "38": "paste-2143188680961.jpg", "70": "rec1486790804.mp3", "42": "rec1486790858.mp3", "43": "paste-7430293422343.jpg", "23": "paste-6158983102706.jpg", "72": "rec1486789502.mp3", "34": "rec1486790589.mp3", "16": "rec1486791310.mp3", "52": "paste-6266357285108.jpg", "32": "paste-687194767612.jpg", "65": "paste-6708738916592.jpg", "2": "paste-7142530613495.jpg", "40": "paste-816043786497.jpg", "1": "paste-1090921693442.jpg", "67": "rec1486791391.mp3", "5": "rec1486790677.mp3", "6": "rec1486791362.mp3", "46": "rec1486790948.mp3", "22": "paste-7073811136753.jpg", "63": "rec1486790918.mp3", "24": "rec1486790620.mp3", "27": "paste-4939212390648.jpg", "45": "paste-6644314407145.jpg", "41": "paste-5811090751727.jpg", "35": "rec1486789528.mp3", "13": "paste-6442450944247.jpg"}

1
backend/lib/utils.js Normal file
View File

@@ -0,0 +1 @@
export {}

23
backend/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "backend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node ./bin/www",
"reinstall": "rm -rf node_modules/ package-lock.json && npm install"
},
"dependencies": {
"adm-zip": "^0.5.16",
"axios": "^1.13.5",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"fs": "^0.0.1-security",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"morgan": "~1.9.1",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
}
}

9
backend/routes/index.js Normal file
View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

View File

@@ -0,0 +1,123 @@
var express = require('express')
var router = express.Router()
const axios = require('axios')
/**
* Services to add:
* - https://unsplash.com/developers
* - https://pixabay.com/api/docs/
*
* Pronounciation will require $2/mo sub:
* https://api.forvo.com/plans-and-pricing/
*/
async function pexelsSearch(query) {
// https://help.pexels.com/hc/en-us/articles/47678194141337-Can-I-change-the-search-language-when-using-the-Pexels-API
// pexels has a monthly rate limit and an hourly rate limit, only the monthly is returned in the headers
// limit is also 200 per hour, will need to manually track that
const resp = await axios.get(
`https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=5`,
{
headers: {
Authorization: process.env.PEXELS_API,
},
}
)
const { data, headers } = resp
const photos = data.photos
let filtered = photos.map((p) => (
{
url: p.src.medium,
desc: p.alt,
credit: p.photographer,
id: p.id
}
))
let response = {
photos: filtered,
ratelimit: {
limit: headers['x-ratelimit-limit'],
remaining: headers['x-ratelimit-remaining'],
reset: new Date(Number(headers['x-ratelimit-reset']) * 1000)
}
}
return response;
}
async function shutterstockSearch(query) {
// shutterstock has an hourly rate limit and no monthly limit
// https://api-reference.shutterstock.com/?shell#assets-api
// higher quality is available with shutterstock watermarks, look into [photo].assets objects
const SHUTTERSTOCK_API_TOKEN = process.env.SHUTTERSTOCK_API
try {
const resp = await axios.get(
'https://api.shutterstock.com/v2/images/search',
{
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${SHUTTERSTOCK_API_TOKEN}`
},
params: {
query,
per_page: 5,
sort: 'popular',
safe: false,
image_type: 'photo'
}
}
)
let { data, headers } = resp
let filtered = data.data.map((p) => (
{
url: p.assets.mosaic.url,
desc: p.description,
credit: p.contributor.id,
id: Number(p.id)
}
))
let response = {
photos: filtered,
ratelimit: {
limit: headers['ratelimit-limit'],
remaining: headers['ratelimit-remaining'],
reset: new Date(Number(headers['ratelimit-reset']))
}
}
return response;
} catch (error) {
console.error('Error:', error);
}``
}
router.get('/', async function(req, res) {
try {
const queryL1 = req.query.queries[0]
// const queryL2 = req.query.queries[1]
// const pexels = await pexelsSearch(queryL1)
const shutterstock = await shutterstockSearch(queryL1)
response = shutterstock;
res.status(200).send(response)
} catch (err) {
console.log(err)
}
});
module.exports = router

9
backend/routes/users.js Normal file
View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
res.status(200).send('respond with a resource');
});
module.exports = router;

8
frontend/.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

10
frontend/.oxlintrc.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "unicorn", "oxc", "vue"],
"env": {
"browser": true
},
"categories": {
"correctness": "error"
}
}

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

44
frontend/README.md Normal file
View File

@@ -0,0 +1,44 @@
# .
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

30
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,30 @@
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from 'eslint-config-prettier/flat'
export default defineConfig([
{
name: 'app/files-to-lint',
files: ['**/*.{vue,js,mjs,jsx}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
skipFormatting,
])

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

43
frontend/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "linguamate",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"reinstall": "rm -rf node_modules/ package-lock.json && npm install",
"lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
"format": "prettier --write --experimental-cli src/"
},
"dependencies": {
"@mdi/font": "^7.4.47",
"axios": "^1.13.5",
"pinia": "^3.0.4",
"primeflex": "^4.0.0",
"vue": "^3.5.27",
"vue-router": "^5.0.1"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@vitejs/plugin-vue": "^6.0.3",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-oxlint": "~1.42.0",
"eslint-plugin-vue": "~10.7.0",
"globals": "^17.3.0",
"npm-run-all2": "^8.0.4",
"oxlint": "~1.42.0",
"prettier": "3.8.1",
"sass-embedded": "^1.97.3",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.3.1",
"vite-plugin-vue-devtools": "^8.0.5"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

9
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
<TheMenu />
<RouterView />
</template>
<script setup>
import TheMenu from '@/components/app/TheMenu.vue'
</script>

View File

@@ -0,0 +1,62 @@
<template>
<Menubar :model="items" class="px-8">
<!-- Menu links -->
<template #item="{ item, props, hasSubmenu }">
<router-link v-if="item.route" v-slot="{ href, navigate }" :to="item.route" custom>
<a :href="href" v-bind="props.action" @click="navigate">
<span :class="item.icon" />
<span>{{ item.label }}</span>
</a>
</router-link>
<a v-else :href="item.url" :target="item.target" v-bind="props.action">
<i :class="item.icon" />
<span>{{ item.label }}</span>
<span v-if="hasSubmenu" class="mdi mdi-chevron-down" />
</a>
</template>
<!-- User profile icon -->
<template #end>
<i class="mdi mdi-account-outline" />
</template>
</Menubar>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{
label: 'Home',
icon: 'mdi mdi-home-outline',
route: '/'
},
{
label: 'Tools',
icon: 'mdi mdi-wrench-outline',
route: '/toolkit'
},
{
label: 'Learn',
icon: 'mdi mdi-lightbulb-outline',
items: [
{
label: 'Pronunciation',
icon: 'mdi mdi-waveform',
route: '/speak'
},
{
label: 'Anki Decks',
icon: 'mdi mdi-card-multiple-outline',
route: '/decks'
},
{
label: 'Self Testing',
icon: 'mdi mdi-send-variant-outline',
route: '/test'
}
]
}
]);
</script>

Some files were not shown because too many files have changed in this diff Show More