Template
feat : working UI for words
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
<script lang="ts">
|
||||
import 'reflect-metadata';
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '~/store/user.store';
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
||||
const req = await userStore.whoami()
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtWelcome />
|
||||
<NuxtLayout>
|
||||
<NuxtLoadingIndicator> Loading... </NuxtLoadingIndicator>
|
||||
<Header />
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { useUserStore } from '~/store/user.store';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div id="self">
|
||||
<template v-if="userStore.has_contacted">
|
||||
<ClientOnly>
|
||||
<template v-if="userStore.self && userStore.self.id">
|
||||
<User :id="userStore.self.id"></User>
|
||||
<button>Logout</button>
|
||||
</template>
|
||||
<button v-else>Login</button>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
Contacting server...
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div id="links">
|
||||
<NuxtLink to="/">Home</NuxtLink>
|
||||
<NuxtLink to="/words">Words</NuxtLink>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
header,
|
||||
header * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
div#self {
|
||||
height: 32px
|
||||
}
|
||||
|
||||
div#links {
|
||||
width: 100%;
|
||||
gap: 1%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { fetchUser } from '~/service/user.service';
|
||||
import { useUserStore } from '~/store/user.store';
|
||||
|
||||
|
||||
export interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const user = await userStore.fetchUser(props.id)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user">
|
||||
<img class="user-avatar" :src="`https://cdn.discordapp.com/avatars/${user?.id}/${user?.avatar}.webp?size=32`">
|
||||
{{ user?.username }}
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
div.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img.user-avatar {
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { WordResponseDTO } from '~/netcode/events/dto/wordresponse.dto';
|
||||
import { useWordStore } from '~/store/word.store';
|
||||
|
||||
export interface Props {
|
||||
word: WordResponseDTO;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const loading = ref(false)
|
||||
const wordStore = useWordStore();
|
||||
|
||||
async function changeWordState() {
|
||||
if (loading.value) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
let data
|
||||
if (props.word.enabled) {
|
||||
data = await wordStore.disableWord(props.word.id)
|
||||
} else {
|
||||
data = await wordStore.enableWord(props.word.id)
|
||||
}
|
||||
loading.value = false
|
||||
props.word.enabled = data.enabled
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="word">
|
||||
<span>{{ word.value }}</span>
|
||||
<input type="checkbox" :class="{ loading }" :checked="word.enabled" :disabled="loading"
|
||||
@click.prevent="changeWordState" />
|
||||
<User :id="word.ownerId"></User>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
div.word {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
input.loading {
|
||||
cursor: wait
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Expose, Type } from 'class-transformer';
|
||||
import { ConceptResponseDTO } from './conceptresponse.dto';
|
||||
|
||||
export class ConceptInTurnResponseDTO {
|
||||
constructor(partial: Partial<ConceptInTurnResponseDTO>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
@Type(() => ConceptResponseDTO)
|
||||
@Expose()
|
||||
concept: ConceptResponseDTO;
|
||||
|
||||
@Expose()
|
||||
order: number;
|
||||
|
||||
@Expose()
|
||||
subconcept: number;
|
||||
|
||||
@Expose()
|
||||
markers: number;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Expose } from 'class-transformer';
|
||||
|
||||
export class ConceptResponseDTO {
|
||||
constructor(partial: Partial<ConceptResponseDTO>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
|
||||
@Expose()
|
||||
id: number;
|
||||
|
||||
@Expose()
|
||||
value: string;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Expose, Type } from 'class-transformer';
|
||||
import { UserResponseDTO } from './userresponse.dto';
|
||||
import { PlayerResponseDTO } from './playerresponse.dto';
|
||||
import { ConceptInTurnResponseDTO } from './conceptinturnresponse.dto';
|
||||
|
||||
export class GameResponseDTO {
|
||||
constructor(partial: Partial<GameResponseDTO>) {
|
||||
Object.assign(this, partial);
|
||||
this.owner = partial.owner;
|
||||
this.players = partial.players;
|
||||
this.currentPlayer = partial.currentPlayer;
|
||||
this.currentConcepts = partial.currentConcepts;
|
||||
}
|
||||
|
||||
@Expose()
|
||||
id: number;
|
||||
|
||||
@Type(() => UserResponseDTO)
|
||||
@Expose()
|
||||
owner: UserResponseDTO;
|
||||
|
||||
@Type(() => PlayerResponseDTO)
|
||||
@Expose()
|
||||
players: PlayerResponseDTO[];
|
||||
|
||||
@Type(() => PlayerResponseDTO)
|
||||
@Expose()
|
||||
currentPlayer: PlayerResponseDTO;
|
||||
|
||||
@Expose()
|
||||
started: boolean;
|
||||
|
||||
@Type(() => ConceptInTurnResponseDTO)
|
||||
@Expose()
|
||||
currentConcepts: ConceptInTurnResponseDTO[];
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Expose, Type } from 'class-transformer';
|
||||
import { UserResponseDTO } from './userresponse.dto';
|
||||
|
||||
export class PlayerResponseDTO {
|
||||
constructor(partial: Partial<PlayerResponseDTO>) {
|
||||
Object.assign(this, partial);
|
||||
this.user = partial.user;
|
||||
}
|
||||
|
||||
@Type(() => UserResponseDTO)
|
||||
@Expose()
|
||||
user: UserResponseDTO;
|
||||
|
||||
@Expose()
|
||||
score: number;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Expose } from 'class-transformer';
|
||||
|
||||
export class UserResponseDTO {
|
||||
constructor(partial: Partial<UserResponseDTO>) {
|
||||
Object.assign(this, partial);
|
||||
this.id = partial.id;
|
||||
}
|
||||
@Expose()
|
||||
id: string;
|
||||
|
||||
@Expose()
|
||||
username: string;
|
||||
|
||||
@Expose()
|
||||
avatar: string;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Expose } from 'class-transformer';
|
||||
|
||||
export class WordResponseDTO {
|
||||
constructor(partial: Partial<WordResponseDTO>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
@Expose()
|
||||
id: number;
|
||||
|
||||
@Expose()
|
||||
value: string;
|
||||
|
||||
@Expose()
|
||||
ownerId: string;
|
||||
|
||||
@Expose()
|
||||
enabled: boolean;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import globals from 'globals';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import js from '@eslint/js';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/.eslintrc.js'],
|
||||
},
|
||||
...compat.extends(
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
'plugin:prettier/recommended',
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslintEslintPlugin,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: '/home/legonzaur/Documents/code/concept/back',
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'require-await': 'off',
|
||||
'@typescript-eslint/require-await': 'error',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
const validPaths = ["/", "/rules", "/test"];
|
||||
const validPaths = ["/", "/rules", "/test", "/words"];
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function subscribe(endpoint: string) {
|
||||
const config = useRuntimeConfig();
|
||||
const evtSource = new EventSource(config.public.endpoint + endpoint, {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
return evtSource;
|
||||
}
|
||||
+6
-1
@@ -1,8 +1,10 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import 'reflect-metadata';
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-11-01',
|
||||
ssr: true,
|
||||
devtools: { enabled: true },
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
endpoint: "https://playtest.legonzaur.fr/api", // can be overridden by NUXT_PUBLIC_ENDPOINT environment variable
|
||||
@@ -10,10 +12,13 @@ export default defineNuxtConfig({
|
||||
devMode: false,
|
||||
},
|
||||
},
|
||||
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: ["/login", "/"],
|
||||
crawlLinks: true,
|
||||
},
|
||||
},
|
||||
|
||||
modules: ["@pinia/nuxt"],
|
||||
})
|
||||
Generated
+65
@@ -7,8 +7,10 @@
|
||||
"name": "nuxt-app",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.9.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"nuxt": "^3.15.0",
|
||||
"pinia": "^2.3.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
@@ -1827,6 +1829,21 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinia/nuxt": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.9.0.tgz",
|
||||
"integrity": "sha512-2yeRo7LeyCF68AbNeL3xu2h6uw0617RkcsYxmA8DJM0R0PMdz5wQHnc44KeENQxR/Mrq8T910XVT6buosqsjBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^3.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pinia": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -6474,6 +6491,28 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz",
|
||||
"integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.3",
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.7.0 || ^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.0.tgz",
|
||||
@@ -9431,6 +9470,32 @@
|
||||
"ufo": "^1.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-devtools-stub": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz",
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.9.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"nuxt": "^3.15.0",
|
||||
"pinia": "^2.3.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Hello
|
||||
</template>
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
<template>
|
||||
Hello
|
||||
Login
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { useWordStore } from '~/store/word.store';
|
||||
|
||||
const wordStore = useWordStore();
|
||||
|
||||
await wordStore.fetchWordList()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="words">
|
||||
<div class="header">
|
||||
<span>Word</span>
|
||||
<span>Status</span>
|
||||
<span>Submitter</span>
|
||||
</div>
|
||||
<Word v-for="word in wordStore.words" :word="word" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
div#words {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
div.header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { UserResponseDTO } from "~/netcode/events/dto/userresponse.dto";
|
||||
|
||||
export async function fetchUser(id: string) {
|
||||
const config = useRuntimeConfig();
|
||||
const data = await useFetch<UserResponseDTO>(config.public.endpoint + `/users/` + id, {
|
||||
credentials: "include",
|
||||
}).then(_d => {
|
||||
if (_d.data.value) {
|
||||
_d.data.value = new UserResponseDTO(_d.data.value)
|
||||
}
|
||||
return _d
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { UserResponseDTO } from "~/netcode/events/dto/userresponse.dto";
|
||||
|
||||
export const useUserStore = defineStore("user", {
|
||||
state: () => ({
|
||||
users: {} as Record<string, UserResponseDTO>,
|
||||
self: null as UserResponseDTO | null,
|
||||
has_contacted: false
|
||||
}),
|
||||
actions: {
|
||||
async fetchUser(id: string) {
|
||||
if (id in this.users) {
|
||||
return this.users[id]
|
||||
}
|
||||
const config = useRuntimeConfig();
|
||||
const data = await useFetch<UserResponseDTO>(config.public.endpoint + `/users/` + id, {
|
||||
credentials: "include",
|
||||
immediate: true
|
||||
})
|
||||
if (data.data.value) {
|
||||
this.users[data.data.value.id] = data.data.value
|
||||
}
|
||||
return data.data.value
|
||||
},
|
||||
async whoami() {
|
||||
const config = useRuntimeConfig();
|
||||
const req = await useFetch<UserResponseDTO>(config.public.endpoint + "/whoami", {
|
||||
credentials: "include",
|
||||
server: false,
|
||||
immediate: true,
|
||||
onResponse: (data) => {
|
||||
this.self = data.response._data
|
||||
this.has_contacted = true
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
return req;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { WordResponseDTO } from "~/netcode/events/dto/wordresponse.dto";
|
||||
|
||||
export const useWordStore = defineStore("word", {
|
||||
state: () => ({
|
||||
words: {} as Record<string, WordResponseDTO>,
|
||||
}),
|
||||
actions: {
|
||||
async fetchWordList() {
|
||||
const config = useRuntimeConfig();
|
||||
const data = await useFetch<WordResponseDTO[]>(config.public.endpoint + `/words/all`, {
|
||||
credentials: "include",
|
||||
})
|
||||
if (data.data.value && !data.error.value) {
|
||||
|
||||
for (const word of data.data.value) {
|
||||
this.words[word.id] = word
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
async enableWord(id: number) {
|
||||
const config = useRuntimeConfig();
|
||||
const data = await $fetch<WordResponseDTO>(config.public.endpoint + `/words/` + id + "/enable", {
|
||||
credentials: "include",
|
||||
method: "PUT"
|
||||
})
|
||||
return data
|
||||
},
|
||||
async disableWord(id: number) {
|
||||
const config = useRuntimeConfig();
|
||||
const data = await $fetch<WordResponseDTO>(config.public.endpoint + `/words/` + id + "/disable", {
|
||||
credentials: "include",
|
||||
method: "PUT"
|
||||
})
|
||||
return data
|
||||
}
|
||||
},
|
||||
});
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"strictPropertyInitialization": false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user