Commit unfinished work

This commit is contained in:
Saphire 2024-07-19 01:09:33 +06:00
commit 96d4f9d675
Signed by: Saphire
GPG Key ID: B26EB7A1F07044C4
36 changed files with 3555 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

40
README.md Normal file
View File

@ -0,0 +1,40 @@
# spacetraders-client
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<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.ts"></script>
</body>
</html>

2477
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "spacetraders-client",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
},
"dependencies": {
"autoprefixer": "^10.4.16",
"pinia": "^2.1.7",
"vue": "^3.3.4",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.5",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/tsconfig": "^0.4.0",
"npm-run-all2": "^6.1.1",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "~5.2.0",
"vite": "^4.4.11",
"vue-tsc": "^1.8.19"
},
"prettier": {
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": false
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

9
src/App.vue Normal file
View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import { RouterLink, RouterView } from "vue-router";
</script>
<template>
<RouterView />
</template>
<style scoped></style>

0
src/api/Factions.ts Normal file
View File

26
src/api/Pagination.ts Normal file
View File

@ -0,0 +1,26 @@
import { type PaginatedResponse } from "@/api/models/PaginatedResponse";
import { get } from "@/utils";
export async function getPaginated<T>(
url: string | URL,
progress?: (current: number, total: number) => void
) {
const entries: T[] = [];
const perPage = 20;
let targetTotal = 21;
const workingUrl = new URL(url);
while (entries.length < targetTotal) {
workingUrl.searchParams.set("limit", perPage + "");
workingUrl.searchParams.set(
"page",
Math.floor(entries.length / perPage) + 1 + ""
);
const page = await get<PaginatedResponse<T>>(workingUrl);
entries.push(...page.data);
targetTotal = page.meta.total;
}
return entries;
}

13
src/api/models/Agent.ts Normal file
View File

@ -0,0 +1,13 @@
import type { LocationSymbol } from "@/api/models/Location";
import type { NominalSymbol } from "./Common";
export type AgentSymbol = NominalSymbol<"Agent">;
export interface Agent {
accountId?: string;
symbol: AgentSymbol;
headquarters: LocationSymbol;
credits: number;
startingFaction: string;
shipCount?: number;
}

18
src/api/models/Common.ts Normal file
View File

@ -0,0 +1,18 @@
// Used for unique IDs, not types!
export type NominalSymbol<T> = string & {
__nominalBrand: T;
};
export interface DescriptiveEnum<EnumType extends string> {
symbol: EnumType;
name: string;
description: string;
}
export interface Descriptive<SymbolType extends NominalSymbol<any>> {
symbol: SymbolType;
name: string;
description: string;
}
export type SymbolOnly<T extends Descriptive<any>> = Pick<T, "symbol">;

108
src/api/models/Enums.ts Normal file
View File

@ -0,0 +1,108 @@
export enum SystemType {
NEUTRON_STAR = "NEUTRON_STAR",
RED_STAR = "RED_STAR",
ORANGE_STAR = "ORANGE_STAR",
BLUE_STAR = "BLUE_STAR",
YOUNG_STAR = "YOUNG_STAR",
WHITE_DWARF = "WHITE_DWARF",
BLACK_HOLE = "BLACK_HOLE",
HYPERGIANT = "HYPERGIANT",
NEBULA = "NEBULA",
UNSTABLE = "UNSTABLE",
}
export enum WaypointType {
PLANET = "PLANET",
GAS_GIANT = "GAS_GIANT",
MOON = "MOON",
ORBITAL_STATION = "ORBITAL_STATION",
JUMP_GATE = "JUMP_GATE",
ASTEROID_FIELD = "ASTEROID_FIELD",
ASTEROID = "ASTEROID",
ENGINEERED_ASTEROID = "ENGINEERED_ASTEROID",
ASTEROID_BASE = "ASTEROID_BASE",
NEBULA = "NEBULA",
DEBRIS_FIELD = "DEBRIS_FIELD",
GRAVITY_WELL = "GRAVITY_WELL",
ARTIFICIAL_GRAVITY_WELL = "ARTIFICIAL_GRAVITY_WELL",
FUEL_STATION = "FUEL_STATION",
}
export enum WaypointTraitType {
UNCHARTED = "UNCHARTED",
UNDER_CONSTRUCTION = "UNDER_CONSTRUCTION",
MARKETPLACE = "MARKETPLACE",
SHIPYARD = "SHIPYARD",
OUTPOST = "OUTPOST",
SCATTERED_SETTLEMENTS = "SCATTERED_SETTLEMENTS",
SPRAWLING_CITIES = "SPRAWLING_CITIES",
MEGA_STRUCTURES = "MEGA_STRUCTURES",
OVERCROWDED = "OVERCROWDED",
HIGH_TECH = "HIGH_TECH",
CORRUPT = "CORRUPT",
BUREAUCRATIC = "BUREAUCRATIC",
TRADING_HUB = "TRADING_HUB",
INDUSTRIAL = "INDUSTRIAL",
BLACK_MARKET = "BLACK_MARKET",
RESEARCH_FACILITY = "RESEARCH_FACILITY",
MILITARY_BASE = "MILITARY_BASE",
SURVEILLANCE_OUTPOST = "SURVEILLANCE_OUTPOST",
EXPLORATION_OUTPOST = "EXPLORATION_OUTPOST",
MINERAL_DEPOSITS = "MINERAL_DEPOSITS",
COMMON_METAL_DEPOSITS = "COMMON_METAL_DEPOSITS",
PRECIOUS_METAL_DEPOSITS = "PRECIOUS_METAL_DEPOSITS",
RARE_METAL_DEPOSITS = "RARE_METAL_DEPOSITS",
METHANE_POOLS = "METHANE_POOLS",
ICE_CRYSTALS = "ICE_CRYSTALS",
EXPLOSIVE_GASES = "EXPLOSIVE_GASES",
STRONG_MAGNETOSPHERE = "STRONG_MAGNETOSPHERE",
VIBRANT_AURORAS = "VIBRANT_AURORAS",
SALT_FLATS = "SALT_FLATS",
CANYONS = "CANYONS",
PERPETUAL_DAYLIGHT = "PERPETUAL_DAYLIGHT",
PERPETUAL_OVERCAST = "PERPETUAL_OVERCAST",
DRY_SEABEDS = "DRY_SEABEDS",
MAGMA_SEAS = "MAGMA_SEAS",
SUPERVOLCANOES = "SUPERVOLCANOES",
ASH_CLOUDS = "ASH_CLOUDS",
VAST_RUINS = "VAST_RUINS",
MUTATED_FLORA = "MUTATED_FLORA",
TERRAFORMED = "TERRAFORMED",
EXTREME_TEMPERATURES = "EXTREME_TEMPERATURES",
EXTREME_PRESSURE = "EXTREME_PRESSURE",
DIVERSE_LIFE = "DIVERSE_LIFE",
SCARCE_LIFE = "SCARCE_LIFE",
FOSSILS = "FOSSILS",
WEAK_GRAVITY = "WEAK_GRAVITY",
STRONG_GRAVITY = "STRONG_GRAVITY",
CRUSHING_GRAVITY = "CRUSHING_GRAVITY",
TOXIC_ATMOSPHERE = "TOXIC_ATMOSPHERE",
CORROSIVE_ATMOSPHERE = "CORROSIVE_ATMOSPHERE",
BREATHABLE_ATMOSPHERE = "BREATHABLE_ATMOSPHERE",
THIN_ATMOSPHERE = "THIN_ATMOSPHERE",
JOVIAN = "JOVIAN",
ROCKY = "ROCKY",
VOLCANIC = "VOLCANIC",
FROZEN = "FROZEN",
SWAMP = "SWAMP",
BARREN = "BARREN",
TEMPERATE = "TEMPERATE",
JUNGLE = "JUNGLE",
OCEAN = "OCEAN",
RADIOACTIVE = "RADIOACTIVE",
MICRO_GRAVITY_ANOMALIES = "MICRO_GRAVITY_ANOMALIES",
DEBRIS_CLUSTER = "DEBRIS_CLUSTER",
DEEP_CRATERS = "DEEP_CRATERS",
SHALLOW_CRATERS = "SHALLOW_CRATERS",
UNSTABLE_COMPOSITION = "UNSTABLE_COMPOSITION",
HOLLOWED_INTERIOR = "HOLLOWED_INTERIOR",
STRIPPED = "STRIPPED",
}
export enum WaypointModifierType {
STRIPPED = "STRIPPED",
UNSTABLE = "UNSTABLE",
RADIATION_LEAK = "RADIATION_LEAK",
CRITICAL_LIMIT = "CRITICAL_LIMIT",
CIVIL_UNREST = "CIVIL_UNREST",
}

12
src/api/models/Faction.ts Normal file
View File

@ -0,0 +1,12 @@
import type { LocationSymbol } from "@/api/models/Location";
import type { Descriptive, NominalSymbol } from "./Common";
export type FactionSymbol = NominalSymbol<"Faction">;
export interface FactionTrait extends Descriptive<any> {}
export interface Faction extends Descriptive<FactionSymbol> {
headquarters: LocationSymbol | "";
traits: FactionTrait[];
isRecruiting: boolean;
}

View File

@ -0,0 +1,51 @@
import type { DescriptiveEnum, NominalSymbol, SymbolOnly } from "./Common";
import type {
SystemType,
WaypointModifierType,
WaypointTraitType,
WaypointType,
} from "./Enums";
import type { Faction, FactionSymbol } from "./Faction";
export type LocationSymbol<T = any> = NominalSymbol<"Location"> & {
__locationType: T;
};
export type SectorSymbol = LocationSymbol<"sector">;
export type SystemSymbol = LocationSymbol<"system">;
export type WaypointSymbol = LocationSymbol<"waypoint">;
export interface Location<
TypeString extends string = string,
SymbolType extends LocationSymbol = LocationSymbol
> {
symbol: SymbolType;
type: TypeString;
x: number;
y: number;
}
export interface System extends Location<SystemType, SystemSymbol> {
sectorSymbol: SectorSymbol;
factions: FactionSymbol[];
}
export interface Waypoint extends Location<WaypointType, WaypointSymbol> {
orbitals: WaypointSymbol[];
orbits?: WaypointSymbol;
}
export interface WaypointDetailed extends Waypoint {
systemSymbol: SystemSymbol;
faction: SymbolOnly<Faction>;
traits: WaypointTrait[];
modifiers: WaypointModifier[];
chart: any;
isUnderConstruction: boolean;
}
export interface WaypointTrait extends DescriptiveEnum<WaypointTraitType> {}
export interface WaypointModifier
extends DescriptiveEnum<WaypointModifierType> {}

View File

@ -0,0 +1,10 @@
export interface PaginatedResponseMeta {
total: number;
page: number;
limit: number;
}
export interface PaginatedResponse<T> {
data: T[];
meta: PaginatedResponseMeta;
}

18
src/assets/base.css Normal file
View File

@ -0,0 +1,18 @@
:root {
color-scheme: dark;
}
body {
--bg-color: #181818;
min-height: 100vh;
color: rgba(235, 235, 235, 0.64);
background: var(--bg-color);
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

23
src/assets/main.css Normal file
View File

@ -0,0 +1,23 @@
@import "./base.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
#app {
display: flex;
font-weight: normal;
margin: 0 auto;
flex: 1 0 auto;
place-items: center;
}
.footer-shown > #app {
margin-bottom: 2em;
}
body {
display: flex;
flex-direction: column;
place-items: center center;
}

View File

390
src/database.ts Normal file
View File

@ -0,0 +1,390 @@
interface Migration {
// Upgrades database structure itself
upgrade: ((event: IDBVersionChangeEvent) => void) | null;
// Migrates the database data to new structure
migrate: (event: any) => void;
}
interface ColumnDefinition {
name: string;
// Assumed to be 1 if missing
version?: number;
unique?: boolean;
multiEntry?: boolean;
// The target of it
foreignTable?: string;
nullable?: boolean;
}
interface TableDefinition {
name: string;
// The db version it first was made at, assumes 1 if missing
version?: number;
// First column is the table key
columns: (ColumnDefinition | string)[];
primaryKeys?: string[];
}
export class IndexedDb {
public versionCurrent: number;
private versionPrevious: number;
public database: IDBDatabase | null = null;
// FK targets and who they come from
// TODO: make this work with migrations?
private foreignKeys: Map<string, string[]> = new Map();
private promise: Promise<IndexedDb>;
// Migrations migrate from version [n] to CURRENT
// Do NOT make them migrate from [n] to [n+1] to ... to CURRENT
constructor(
name: string,
version: number,
public tables: TableDefinition[],
public migrations: Migration[] = []
) {
let resolve: (result: IndexedDb) => void;
let reject: (reason: any) => void;
this.promise = new Promise<IndexedDb>(
(resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
}
);
this.versionCurrent = version;
this.versionPrevious = version;
var request = window.indexedDB.open(name, this.versionCurrent);
request.addEventListener("success", (e: any) => {
this.success(e);
resolve(this);
});
request.addEventListener("upgradeneeded", (e: IDBVersionChangeEvent) =>
this.upgradeNeeded(e)
);
request.addEventListener("error", (a: any) => {
this.failure(a);
reject(a);
});
}
public init(): Promise<IndexedDb> {
return this.promise;
}
public failure(error: any) {
console.error(
"Database access request failed. No recovery possible.",
error
);
}
public success(event: any) {
this.database = event.target.result;
if (!this.database) {
throw new Error("Unable to store database reference");
}
if (
this.versionPrevious == this.versionCurrent ||
!this.migrations[this.versionPrevious]
) {
console.log("Database opened successfully");
return;
} else {
this.migrations[this.versionPrevious].migrate(event);
}
}
public getColumnName(column: string | ColumnDefinition) {
if (typeof column === "object") return column.name;
const cleanName = column.replace(/(:[0-9]+)?\??$/, "");
return cleanName;
}
public upgradeNeeded(event: IDBVersionChangeEvent) {
this.database = (event.target as IDBOpenDBRequest).result;
let migration: Migration = this.migrations[event.oldVersion];
if (migration && migration.upgrade != null) {
migration.upgrade(event);
} else {
this.tables.forEach((table) => {
let objectStore: IDBObjectStore | null = null;
if ((table.version ?? 1) > event.oldVersion) {
const firstCol = table.columns[0];
objectStore = this.database!.createObjectStore(table.name, {
keyPath: table.primaryKeys
? table.primaryKeys
: typeof firstCol === "object"
? firstCol.name
: firstCol,
});
} else {
objectStore =
(
event.target as IDBOpenDBRequest
).transaction?.objectStore(table.name) ?? null;
}
if (objectStore == null)
throw new Error("Null object store for " + table.name);
for (let index = 0; index < table.columns.length; index++) {
const column = table.columns[index];
const columnVersion =
typeof column === "object"
? column.version ?? 1
: +(
column
.match(/^[^:]+(:[0-9]+)(\?)?$/)?.[1]
.substring(1) ?? "1"
);
if (columnVersion > event.oldVersion) {
if (typeof column === "object") {
objectStore.createIndex(column.name, column.name, {
unique: column.unique == true,
multiEntry: column.multiEntry == true,
});
} else {
const cleanName = column.replace(
/(:[0-9]+)?\??$/,
""
);
objectStore.createIndex(cleanName, cleanName);
}
}
}
});
}
this.versionPrevious = event.oldVersion;
}
async get<Type>(
table: string,
key: string | number | IDBKeyRange
): Promise<Type | undefined> {
return await new Promise((resolve, reject) => {
const transaction = this.database!.transaction(table, "readonly");
transaction.addEventListener("error", (error) => {
console.error(
`Database transaction failed for ${table}`,
error
);
reject(error);
});
const request = transaction.objectStore(table).get(key);
request.addEventListener("error", (error) => {
console.error(
`Database GET request failed for ${table}["${key}"]`,
error
);
reject(error);
});
request.addEventListener("success", () => {
resolve(<any>request.result);
});
});
}
async getList<Type>(
table: string,
value?: string | IDBKeyRange,
column: string | null = null
): Promise<Type[]> {
return await new Promise((resolve, reject) => {
const transaction = this.database!.transaction(table, "readonly");
transaction.addEventListener("error", (error) => {
console.error(
`Database transaction failed for ${table}`,
error
);
reject(error);
});
let request: IDBRequest<IDBCursorWithValue | null>;
if (column)
request = transaction
.objectStore(table)
.index(column)
.openCursor(value);
else request = transaction.objectStore(table).openCursor(value);
request.addEventListener("error", (error) => {
console.error(
`Database GETLIST request failed for ${table}["${value}"]`,
error
);
reject(error);
});
const list: Type[] = [];
request.addEventListener("success", (event) => {
let cursor: IDBCursorWithValue = (<any>event.target!).result;
if (cursor) {
list.push(cursor.value);
cursor.continue();
} else {
resolve(list);
}
});
});
}
public async insert(tableName: string, object: any) {
let table = this.tables.filter(
(t) => t.name.toLowerCase() == tableName.toLowerCase()
)[0];
if (!table) {
throw new Error(`Invalid table ${tableName}`);
}
const col = table.columns[0];
const key = typeof col === "object" ? col.name : col;
const check = await this.get(tableName, object[key]);
if (check) {
throw new Error(
`Cannot INSERT object ${object[key]}, already exist in ${tableName}`
);
}
return await this.set(tableName, object);
}
public async update(tableName: string, object: any) {
let table = this.tables.filter(
(t) => t.name.toLowerCase() == tableName.toLowerCase()
)[0];
if (!table) {
throw new Error(`Invalid table ${tableName}`);
}
const col = table.columns[0];
const key = typeof col === "object" ? col.name : col;
const check = await this.get(tableName, object[key]);
if (!check) {
throw new Error(
`Cannot UPDATE object ${object[key]}, does not exist in ${tableName}`
);
}
return await this.set(tableName, object);
}
private async rawSet(tableName: string, object: any): Promise<void> {
return await new Promise((resolve, reject) => {
object.dbVersion = this.versionCurrent;
var transaction = this.database!.transaction(
tableName,
"readwrite"
);
transaction.addEventListener("error", (error) => {
console.error(
`Database transaction failed for ${tableName}`,
error
);
reject(error);
});
var request = transaction.objectStore(tableName).put(object);
request.addEventListener("error", (error) => {
console.error(
`Database SET request failed for ${tableName}`,
error
);
reject(error);
});
request.addEventListener("success", () => {
resolve();
});
});
}
public async set<Type>(tableName: string, object: Type): Promise<void> {
let table = this.tables.filter(
(t) => t.name.toLowerCase() == tableName.toLowerCase()
)[0];
if (!table) {
throw new Error(`Invalid table ${tableName}`);
}
for (const key in object) {
if ((<any>object).hasOwnProperty(key)) {
if (
key != "dbVersion" &&
table.columns.filter((col) =>
typeof col === "object" ? col.name == key : col == key
).length <= 0
) {
throw new Error(`Invalid object has extra key ${key}`);
}
}
}
let stop = false;
for (let i = 0; i < table.columns.length; i++) {
const col = table.columns[i];
const name = typeof col === "object" ? col.name : col;
const value = (<any>object)[name];
if (value === null || value === undefined) {
if (
(typeof col === "string" && !col.endsWith("?")) ||
(typeof col === "object" && !col.nullable)
) {
throw new Error(
`Non-nullable column ${name} is null or undefined`
);
}
delete (<any>object)[name];
continue;
}
if (typeof col === "object" && col.foreignTable) {
let foreign = await this.get(col.foreignTable, value);
if (!foreign) {
console.error(`Foreign key ${name} is invalid`);
//throw new Error(`Foreign key ${name} is invalid`)
}
}
}
return await this.rawSet(table.name, object);
}
public async deleteAll(
tableName: string,
value?: number | string | IDBKeyRange,
column?: string
) {
return await new Promise<void>((resolve, reject) => {
var transaction = this.database!.transaction(
tableName,
"readwrite"
);
transaction.addEventListener("error", (error) => {
console.error(
`Database transaction failed for ${tableName}`,
error
);
reject(error);
});
let request: IDBRequest<IDBCursorWithValue | null>;
if (column)
request = transaction
.objectStore(tableName)
.index(column)
.openCursor(value);
else request = transaction.objectStore(tableName).openCursor(value);
request.addEventListener("error", (error) => {
console.error(
`Database DELETE request failed for ${tableName}["${value}"]`,
error
);
reject(error);
});
request.addEventListener("success", (event) => {
let cursor: IDBCursorWithValue = (<any>event.target!).result;
if (cursor) {
cursor.delete();
cursor.continue();
} else {
resolve();
}
});
});
}
}

15
src/main.ts Normal file
View File

@ -0,0 +1,15 @@
import "./assets/main.css";
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "@/App.vue";
import router from "@/router";
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount("#app");

25
src/router.ts Normal file
View File

@ -0,0 +1,25 @@
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "@/views/HomeView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/user",
name: "user",
component: () => import("@/views/UserView.vue"),
},
{
path: "/register",
name: "registration",
component: () => import("@/views/RegistrationView.vue"),
},
],
});
export default router;

17
src/stores/auth.ts Normal file
View File

@ -0,0 +1,17 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { Agent } from '@/api/models/Agent'
export const useAuthStore = defineStore('auth', () => {
const user = ref<null | Agent>(null)
async function login() {
}
async function register() {
}
return { user, login, register }
})

40
src/stores/systems.ts Normal file
View File

@ -0,0 +1,40 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
import type { Agent } from "@/api/models/Agent";
import { IndexedDb } from "@/database";
import type { System } from "@/api/models/Location";
export const useAuthStore = defineStore("systems", () => {
const db = new IndexedDb("SpacetraderClient", 1, [
{
name: "systems",
// Gonna assume that sectorSymbol is just a prefix of symbol... for now?
columns: ["symbol", "type", "x", "y"],
},
{
name: "waypoints",
columns: [
"symbol",
"type",
"x",
"y",
{
name: "orbits",
// Will need to make sure stuff is inserted one after another...
foreignTable: "waypoints",
nullable: true,
},
{
name: "system",
foreignTable: "systems",
},
],
},
]);
const cachedSystems = ref<null | System>(null);
async function getData() {}
return { cachedSystems, getData };
});

60
src/utils.ts Normal file
View File

@ -0,0 +1,60 @@
export function fromHex(input: string) {
const numbers = input
.match(/[0-9a-zA-Z]{2,2}/g)
?.map((v) => parseInt(v, 16));
if (!numbers) throw new Error("Input cannot be parsed as hex string");
return new Uint8Array(numbers);
}
export async function get<T>(
url: string | URL,
headers?: Record<string, string>
): Promise<T> {
return send(url, undefined, "GET", headers);
}
export async function send<Treceived, Tsent = any | undefined>(
url: string | URL,
data?: Tsent,
method: "POST" | "PUT" | "PATCH" | "DELETE" | "GET" = "POST",
headers?: Record<string, string>
): Promise<Treceived> {
if (fetch) {
return fetch(url, {
method: method,
headers: data
? {
"Content-Type": "application/json",
...headers,
}
: headers,
body: data ? JSON.stringify(data) : undefined,
}).then((response) => {
if (!response.ok) return Promise.reject(response);
return response.json();
});
}
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
for (const header in headers ?? {}) {
if (
!headers ||
!Object.prototype.hasOwnProperty.call(headers, header)
)
continue;
const value = headers[header];
xhr.setRequestHeader(header, value);
}
xhr.open(method, url, true);
xhr.addEventListener("load", function (e) {
resolve(JSON.parse(this.response));
});
xhr.addEventListener("error", function (e) {
reject(e);
});
if (data) xhr.send(JSON.stringify(data));
else xhr.send();
});
}

5
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<main></main>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { getPaginated } from "@/api/Pagination";
import type { Faction } from "@/api/models/Faction";
import type { PaginatedResponse } from "@/api/models/PaginatedResponse";
import { get } from "@/utils";
import { onMounted, ref } from "vue";
const factions = ref<Faction[]>([]);
onMounted(async () => {
factions.value.push(
...(await getPaginated<Faction>(
"https://api.spacetraders.io/v2/factions"
))
);
});
</script>
<template>
<main class="container max-w-4xl mx-auto">
<h2 class="my-3">Factions</h2>
<div class="space-y-4">
<div
class="bg-slate-500 rounded p-4 space-y-2"
v-for="faction in factions"
>
<h3>{{ faction.name }} ({{ faction.symbol }})</h3>
<div v-if="!faction.isRecruiting">NOT RECRUITING</div>
<div>{{ faction.description }}</div>
<div>Headquarters system: {{ faction.headquarters }}</div>
<ul class="ml-4 space-y-2">
<li v-for="trait in faction.traits" class="border-l pl-2">
<span>{{ trait.name }}</span> -
{{ trait.description }}
</li>
</ul>
</div>
</div>
</main>
</template>

0
src/views/UserView.vue Normal file
View File

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {
},
},
plugins: [],
};

13
tsconfig.app.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"experimentalDecorators": true
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

17
tsconfig.node.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"experimentalDecorators": true,
"types": ["node"]
}
}

20
vite.config.ts Normal file
View File

@ -0,0 +1,20 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 5283,
}
})