Commit unfinished work
This commit is contained in:
commit
96d4f9d675
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal 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
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
40
README.md
Normal file
40
README.md
Normal 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
|
||||
```
|
13
index.html
Normal file
13
index.html
Normal 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
2477
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal 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
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
9
src/App.vue
Normal file
9
src/App.vue
Normal 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
0
src/api/Factions.ts
Normal file
26
src/api/Pagination.ts
Normal file
26
src/api/Pagination.ts
Normal 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
13
src/api/models/Agent.ts
Normal 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
18
src/api/models/Common.ts
Normal 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
108
src/api/models/Enums.ts
Normal 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
12
src/api/models/Faction.ts
Normal 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;
|
||||
}
|
51
src/api/models/Location.ts
Normal file
51
src/api/models/Location.ts
Normal 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> {}
|
10
src/api/models/PaginatedResponse.ts
Normal file
10
src/api/models/PaginatedResponse.ts
Normal 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
18
src/assets/base.css
Normal 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
1
src/assets/logo.svg
Normal 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
23
src/assets/main.css
Normal 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;
|
||||
}
|
0
src/components/Accordion.vue
Normal file
0
src/components/Accordion.vue
Normal file
390
src/database.ts
Normal file
390
src/database.ts
Normal 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
15
src/main.ts
Normal 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
25
src/router.ts
Normal 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
17
src/stores/auth.ts
Normal 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
40
src/stores/systems.ts
Normal 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
60
src/utils.ts
Normal 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
5
src/views/HomeView.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
40
src/views/RegistrationView.vue
Normal file
40
src/views/RegistrationView.vue
Normal 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
0
src/views/UserView.vue
Normal file
9
tailwind.config.js
Normal file
9
tailwind.config.js
Normal 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
13
tsconfig.app.json
Normal 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
11
tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
17
tsconfig.node.json
Normal file
17
tsconfig.node.json
Normal 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
20
vite.config.ts
Normal 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,
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user