mirror of
https://github.com/ThisIsBenny/wishlist-app.git
synced 2025-04-19 15:27:41 +00:00
first commit
Signed-off-by: Benny Samir Hierl <bennysamir@posteo.de>
This commit is contained in:
commit
38600ebacb
62 changed files with 13201 additions and 0 deletions
10
.dockerignore
Normal file
10
.dockerignore
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
dist/
|
||||||
|
.vscode
|
||||||
|
node_modules
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.gitignore
|
||||||
|
.editorconfig
|
||||||
|
README.md
|
||||||
|
prisma/seed.ts
|
||||||
|
data/
|
13
.editorconfig
Normal file
13
.editorconfig
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
3
.env.template
Normal file
3
.env.template
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
NODE_ENV=development
|
||||||
|
VITE_API_BASEURL=http://localhost:5000/api
|
||||||
|
DATABASE_URL="file:../data/data.db"
|
16
.eslintrc.cjs
Normal file
16
.eslintrc.cjs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-typescript/recommended',
|
||||||
|
'@vue/eslint-config-prettier',
|
||||||
|
],
|
||||||
|
plugins: ['prettier'],
|
||||||
|
env: {
|
||||||
|
'vue/setup-compiler-macros': true,
|
||||||
|
},
|
||||||
|
}
|
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# 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
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
dist/
|
||||||
|
.vscode
|
||||||
|
coverage
|
||||||
|
.env
|
||||||
|
data
|
||||||
|
prisma/seed.ts
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
33
Dockerfile
Normal file
33
Dockerfile
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Build artifacts
|
||||||
|
FROM node:lts as builder
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json /app/
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:lts
|
||||||
|
|
||||||
|
LABEL maintainer="github.com/thisisbenny"
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=5000
|
||||||
|
ENV DATABASE_URL="file:../data/data.db"
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN mkdir data
|
||||||
|
|
||||||
|
COPY package.json package-lock.json /app/
|
||||||
|
COPY ./prisma /app/prisma
|
||||||
|
RUN npm ci
|
||||||
|
COPY --from=builder /app/dist /app
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
ENTRYPOINT npx prisma migrate deploy && node api/server.js
|
52
README.md
Normal file
52
README.md
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# wishlist
|
||||||
|
|
||||||
|
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=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.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=johnsoncodehk.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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
wishlist:
|
||||||
|
build: ./
|
||||||
|
ports:
|
||||||
|
- '5000:5000'
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
9
env.d.ts
vendored
Normal file
9
env.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_BASEURL: string
|
||||||
|
// more env variables...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
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>Wunschlisten</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
nodemon.json
Normal file
4
nodemon.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"ignore": [".git", "node_modules/**/node_modules"],
|
||||||
|
"watch": ["src/api/"]
|
||||||
|
}
|
12118
package-lock.json
generated
Normal file
12118
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
56
package.json
Normal file
56
package.json
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently --kill-others \"npm run dev:frontend\" \"npm run dev:backend\"",
|
||||||
|
"dev:frontend": "vite",
|
||||||
|
"dev:backend": "nodemon -r dotenv/config ./src/api/server.ts",
|
||||||
|
"build": "npm run build:frontend && npm run build:backend",
|
||||||
|
"build:frontend": "vue-tsc --noEmit && vite build",
|
||||||
|
"build:backend": "tsc -p tsconfig.backend.json --outDir dist",
|
||||||
|
"preview": "vite preview --port 5050",
|
||||||
|
"test:unit": "vitest --environment jsdom",
|
||||||
|
"test:unit:ci": "vitest --environment jsdom --run",
|
||||||
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^3.8.1",
|
||||||
|
"axios": "^0.25.0",
|
||||||
|
"fastify": "^3.27.0",
|
||||||
|
"fastify-compress": "^4.0.1",
|
||||||
|
"fastify-cors": "^6.0.2",
|
||||||
|
"fastify-helmet": "^7.0.1",
|
||||||
|
"fastify-static": "^4.5.0",
|
||||||
|
"vue": "^3.2.27",
|
||||||
|
"vue-router": "^4.0.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.1.0",
|
||||||
|
"@types/node": "^16.11.21",
|
||||||
|
"@vitejs/plugin-vue": "^2.0.1",
|
||||||
|
"@vue/eslint-config-prettier": "^7.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^10.0.0",
|
||||||
|
"@vue/test-utils": "^2.0.0-rc.18",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
|
"concurrently": "^7.0.0",
|
||||||
|
"dotenv": "^14.3.2",
|
||||||
|
"eslint": "^8.5.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-vue": "^8.2.0",
|
||||||
|
"jsdom": "^19.0.0",
|
||||||
|
"nodemon": "^2.0.15",
|
||||||
|
"pino-pretty": "^7.5.1",
|
||||||
|
"postcss": "^8.4.5",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"prisma": "^3.8.1",
|
||||||
|
"tailwindcss": "^3.0.15",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"typescript": "~4.5.4",
|
||||||
|
"vite": "^2.7.13",
|
||||||
|
"vitest": "^0.1.23",
|
||||||
|
"vue-tsc": "^0.29.8"
|
||||||
|
}
|
||||||
|
}
|
7
postcss.config.js
Normal file
7
postcss.config.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Wishlist" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"imageSrc" TEXT NOT NULL,
|
||||||
|
"slugUrlText" TEXT NOT NULL,
|
||||||
|
"description" TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Item" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"url" TEXT,
|
||||||
|
"image" TEXT,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"comment" TEXT,
|
||||||
|
"bought" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"wishlistId" TEXT,
|
||||||
|
CONSTRAINT "Item_wishlistId_fkey" FOREIGN KEY ("wishlistId") REFERENCES "Wishlist" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Wishlist_slugUrlText_key" ON "Wishlist"("slugUrlText");
|
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "sqlite"
|
32
prisma/schema.prisma
Normal file
32
prisma/schema.prisma
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Wishlist {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
title String
|
||||||
|
imageSrc String
|
||||||
|
slugUrlText String @unique
|
||||||
|
description String?
|
||||||
|
items Item[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Item {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
url String?
|
||||||
|
image String?
|
||||||
|
description String
|
||||||
|
comment String?
|
||||||
|
bought Boolean @default(false)
|
||||||
|
wishlist Wishlist? @relation(fields: [wishlistId], references: [id])
|
||||||
|
wishlistId String?
|
||||||
|
}
|
BIN
public/benny.jpeg
Normal file
BIN
public/benny.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
public/jonas.jpeg
Normal file
BIN
public/jonas.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
public/nadine.jpeg
Normal file
BIN
public/nadine.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
27
src/App.vue
Normal file
27
src/App.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<div class="app max-w-[900px] mx-auto p-10">
|
||||||
|
<main>
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<template v-if="Component">
|
||||||
|
<keep-alive>
|
||||||
|
<suspense>
|
||||||
|
<component :is="Component"></component>
|
||||||
|
<template #fallback>
|
||||||
|
<div
|
||||||
|
class="flex flex-row space-x-2 items-center content-center justify-center m-20"
|
||||||
|
>
|
||||||
|
<IconSpinner class="w-4 h-4" />
|
||||||
|
<span> Lade Daten... </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</suspense>
|
||||||
|
</keep-alive>
|
||||||
|
</template>
|
||||||
|
</router-view>
|
||||||
|
</main>
|
||||||
|
<modal-overlay />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconSpinner from '@/components/icons/IconSpinner.vue'
|
||||||
|
</script>
|
26
src/api/app.ts
Normal file
26
src/api/app.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import staticFiles from 'fastify-static'
|
||||||
|
import path from 'path'
|
||||||
|
import { initApp } from './config'
|
||||||
|
import routes from './routes'
|
||||||
|
|
||||||
|
const build = async (opts = {}) => {
|
||||||
|
const app = await initApp(opts)
|
||||||
|
|
||||||
|
routes.register(app)
|
||||||
|
|
||||||
|
app.register(staticFiles, {
|
||||||
|
root: path.join(__dirname, '..', 'static'),
|
||||||
|
})
|
||||||
|
app.setNotFoundHandler((req, res) => {
|
||||||
|
res.sendFile('index.html')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/healthz', async () => {
|
||||||
|
return { status: 'ok' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: disconnet prisma client when server will be shutdown
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
export default build
|
20
src/api/config/fastify.ts
Normal file
20
src/api/config/fastify.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
let logLevel
|
||||||
|
switch (process.env.NODE_ENV) {
|
||||||
|
case 'development':
|
||||||
|
logLevel = 'debug'
|
||||||
|
break
|
||||||
|
case 'test':
|
||||||
|
logLevel = 'silent'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
logLevel = 'info'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
logger: {
|
||||||
|
level: logLevel,
|
||||||
|
prettyPrint: process.env.NODE_ENV === 'development',
|
||||||
|
redact: ['err.stack'],
|
||||||
|
},
|
||||||
|
}
|
2
src/api/config/index.ts
Normal file
2
src/api/config/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as fastify } from './fastify'
|
||||||
|
export { default as initApp } from './initApp'
|
25
src/api/config/initApp.ts
Normal file
25
src/api/config/initApp.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import helmet from 'fastify-helmet'
|
||||||
|
import Fastify, { FastifyContextConfig } from 'fastify'
|
||||||
|
import compress from 'fastify-compress'
|
||||||
|
import cors from 'fastify-cors'
|
||||||
|
import { fastify as defaultConfig } from './'
|
||||||
|
|
||||||
|
export default async (opts: FastifyContextConfig = {}) => {
|
||||||
|
const app = Fastify({
|
||||||
|
...defaultConfig,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
|
||||||
|
await app.register(helmet, {
|
||||||
|
contentSecurityPolicy: false,
|
||||||
|
crossOriginEmbedderPolicy: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
await app.register(cors, {
|
||||||
|
origin: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await app.register(compress)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
1
src/api/models/index.ts
Normal file
1
src/api/models/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as wishlist } from './wishlist'
|
32
src/api/models/wishlist/index.ts
Normal file
32
src/api/models/wishlist/index.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { prisma } from '../../services'
|
||||||
|
import { Wishlist, WishlistItem } from '../../../types'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAll: async (): Promise<any> => {
|
||||||
|
return await prisma.client.wishlist.findMany({
|
||||||
|
include: { items: false },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getBySlugUrlText: async (
|
||||||
|
value: string,
|
||||||
|
includeItems = false
|
||||||
|
): Promise<any> => {
|
||||||
|
return await prisma.client.wishlist.findUnique({
|
||||||
|
where: {
|
||||||
|
slugUrlText: value,
|
||||||
|
},
|
||||||
|
include: { items: includeItems },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
updateItem: async (itemId: number, payload: any) => {
|
||||||
|
return await prisma.client.item.update({
|
||||||
|
where: {
|
||||||
|
id: itemId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
13
src/api/routes/index.ts
Normal file
13
src/api/routes/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
import { default as wishlistRoute } from './wishlist/'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
register: (app: FastifyInstance) => {
|
||||||
|
return app.register(
|
||||||
|
async (app) => {
|
||||||
|
await app.register(wishlistRoute, { prefix: '/wishlist' })
|
||||||
|
},
|
||||||
|
{ prefix: '/api' }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
9
src/api/routes/wishlist/index.ts
Normal file
9
src/api/routes/wishlist/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
import { getAll, getBySlugUrl } from './read'
|
||||||
|
import { updateItem } from './update'
|
||||||
|
|
||||||
|
export default async (app: FastifyInstance) => {
|
||||||
|
await app.route(getAll)
|
||||||
|
await app.route(getBySlugUrl)
|
||||||
|
await app.route(updateItem)
|
||||||
|
}
|
77
src/api/routes/wishlist/read.ts
Normal file
77
src/api/routes/wishlist/read.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { FastifyRequest, FastifyReply, RouteOptions } from 'fastify'
|
||||||
|
import { wishlist } from '../../models'
|
||||||
|
|
||||||
|
export const getAll = <any>{
|
||||||
|
method: 'GET',
|
||||||
|
url: '/',
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string' },
|
||||||
|
title: { type: 'string' },
|
||||||
|
imageSrc: { type: 'string' },
|
||||||
|
description: { type: 'string' },
|
||||||
|
slugUrlText: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
return await wishlist.getAll()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetBySlugUrlTextRequest extends FastifyRequest {
|
||||||
|
params: {
|
||||||
|
slugText: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBySlugUrl = <RouteOptions>{
|
||||||
|
method: 'GET',
|
||||||
|
url: '/:slugText',
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string' },
|
||||||
|
title: { type: 'string' },
|
||||||
|
imageSrc: { type: 'string' },
|
||||||
|
description: { type: 'string' },
|
||||||
|
slugUrlText: { type: 'string' },
|
||||||
|
items: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number' },
|
||||||
|
title: { type: 'string' },
|
||||||
|
url: { type: 'string' },
|
||||||
|
image: { type: 'string' },
|
||||||
|
description: { type: 'string' },
|
||||||
|
comment: { type: 'string' },
|
||||||
|
bought: { type: 'boolean' },
|
||||||
|
wishlistId: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (request: GetBySlugUrlTextRequest, reply: FastifyReply) => {
|
||||||
|
const list = await wishlist.getBySlugUrlText(request.params.slugText, true)
|
||||||
|
if (list) {
|
||||||
|
return list
|
||||||
|
} else {
|
||||||
|
return reply.code(404).send({
|
||||||
|
error: 'notFound',
|
||||||
|
http: 404,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
44
src/api/routes/wishlist/update.ts
Normal file
44
src/api/routes/wishlist/update.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { FastifyRequest, FastifyReply, RouteOptions } from 'fastify'
|
||||||
|
import { wishlist } from '../../models'
|
||||||
|
|
||||||
|
interface GetBySlugUrlTextRequest extends FastifyRequest {
|
||||||
|
params: {
|
||||||
|
wishlistId: string
|
||||||
|
itemId: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateItem = <RouteOptions>{
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/:wishlistId/item/:itemId',
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
204: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
title: { type: 'string' },
|
||||||
|
url: { type: 'string' },
|
||||||
|
image: { type: 'string' },
|
||||||
|
description: { type: 'string' },
|
||||||
|
comment: { type: 'string' },
|
||||||
|
bought: { type: 'boolean' },
|
||||||
|
wishlistId: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (request: GetBySlugUrlTextRequest, reply: FastifyReply) => {
|
||||||
|
const item = await wishlist.updateItem(
|
||||||
|
Number(request.params.itemId),
|
||||||
|
request.body
|
||||||
|
)
|
||||||
|
if (item) {
|
||||||
|
return item
|
||||||
|
} else {
|
||||||
|
return reply.code(404).send({
|
||||||
|
error: 'notFound',
|
||||||
|
http: 404,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
11
src/api/server.ts
Normal file
11
src/api/server.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import fastifyBuilder from './app'
|
||||||
|
|
||||||
|
fastifyBuilder().then((app) => {
|
||||||
|
app.listen(process.env.PORT || 5000, '0.0.0.0', (err, address) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log(`Server listening at ${address}`)
|
||||||
|
})
|
||||||
|
})
|
1
src/api/services/index.ts
Normal file
1
src/api/services/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as prisma } from './prisma'
|
7
src/api/services/prisma/index.ts
Normal file
7
src/api/services/prisma/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
const client = new PrismaClient()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
client,
|
||||||
|
}
|
3
src/assets/tailwind.css
Normal file
3
src/assets/tailwind.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
15
src/components/BaseButton.vue
Normal file
15
src/components/BaseButton.vue
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
icon?: any
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="border-2 border-stone-200 text-stone-500 hover:bg-stone-100 rounded-md py-1 px-2 flex-row items-center w-fit inline-flex justify-center"
|
||||||
|
>
|
||||||
|
<component v-if="icon" :is="icon" class="w-4 h-4 mr-1" />
|
||||||
|
<span>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
50
src/components/Modal.vue
Normal file
50
src/components/Modal.vue
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseButton from './BaseButton.vue'
|
||||||
|
import { useModal } from '@/composables'
|
||||||
|
|
||||||
|
const modal = useModal()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="modal.isShown"
|
||||||
|
class="fixed z-10 inset-0 overflow-y-auto"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
|
||||||
|
>
|
||||||
|
<div class="fixed inset-0 bg-gray-500/75 transition-opacity"></div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||||
|
>​</span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||||
|
>
|
||||||
|
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ modal.title }}
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ modal.text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 px-4 py-3 sm:px-6 flex flex-row">
|
||||||
|
<BaseButton class="w-full" @click="modal.confirm">{{
|
||||||
|
modal.confirmText
|
||||||
|
}}</BaseButton>
|
||||||
|
<BaseButton class="w-full ml-2" @click="modal.cancel">{{
|
||||||
|
modal.cancelText
|
||||||
|
}}</BaseButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
18
src/components/Tile.vue
Normal file
18
src/components/Tile.vue
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
title?: string
|
||||||
|
imageSrc: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative w-40 h-40 rounded-full overflow-hidden">
|
||||||
|
<img :src="imageSrc" class="object-cover w-full h-full" :alt="title" />
|
||||||
|
<div
|
||||||
|
v-if="title"
|
||||||
|
class="absolute w-full py-2.5 bottom-0 inset-x-0 bg-white opacity-60 text-sm font-bold text-center leading-4"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
55
src/components/WishlistItem.vue
Normal file
55
src/components/WishlistItem.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import IconLink from './icons/IconLink.vue'
|
||||||
|
import IconImagePlaceholder from './icons/IconImagePlaceholder.vue'
|
||||||
|
import BaseButton from './BaseButton.vue'
|
||||||
|
import IconCart from './icons/IconCart.vue'
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
image: string
|
||||||
|
url?: string
|
||||||
|
description: string
|
||||||
|
comment?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="h-fit sm:h-40 flex flex-col sm:flex-row space-x-0 sm:space-x-2 rounded-md border-stone-200 border-2 overflow-hidden"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="image"
|
||||||
|
:src="image"
|
||||||
|
:alt="title"
|
||||||
|
class="object-cover sm:aspect-[3/2] max-h-44 flex-shrink-0 flex-grow-0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="sm:aspect-[3/2] max-h-44 flex-shrink-0 flex-grow-0 bg-stone-100 flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<IconImagePlaceholder class="h-36 w-36 opacity-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col p-2 justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-lg mb-1 font-bold">{{ title }}</h1>
|
||||||
|
<p class="text-sm">{{ description }}</p>
|
||||||
|
<a
|
||||||
|
v-if="url"
|
||||||
|
:href="url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="text-sm mt-1 text-stone-500 flex flex-row items-center w-fit"
|
||||||
|
>
|
||||||
|
<IconLink class="mr-1 w-4 h-4" />
|
||||||
|
<span>Produktseite öffnen</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
class="mt-4 sm:mt-2 text-xs"
|
||||||
|
:icon="IconCart"
|
||||||
|
@click="$emit('bought')"
|
||||||
|
>Gekauft</BaseButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
8
src/components/icons/IconCart.vue
Normal file
8
src/components/icons/IconCart.vue
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
8
src/components/icons/IconImagePlaceholder.vue
Normal file
8
src/components/icons/IconImagePlaceholder.vue
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M14,6L10.25,11L13.1,14.8L11.5,16C9.81,13.75 7,10 7,10L1,18H23L14,6Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
8
src/components/icons/IconLink.vue
Normal file
8
src/components/icons/IconLink.vue
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
5
src/components/icons/IconSpinner.vue
Normal file
5
src/components/icons/IconSpinner.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<svg class="animate-spin" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
3
src/composables/index.ts
Normal file
3
src/composables/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './useWishlistsStore'
|
||||||
|
export * from './useWishlistStore'
|
||||||
|
export * from './useModal'
|
32
src/composables/useModal.ts
Normal file
32
src/composables/useModal.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
let _resolve: (confirmed: boolean) => void
|
||||||
|
|
||||||
|
const callback = (confirmed: boolean) => {
|
||||||
|
data.isShown = false
|
||||||
|
_resolve(confirmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = (title: string, text = '') => {
|
||||||
|
data.title = title
|
||||||
|
data.text = text
|
||||||
|
data.isShown = true
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
_resolve = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = reactive({
|
||||||
|
isShown: false,
|
||||||
|
show,
|
||||||
|
title: '',
|
||||||
|
text: '',
|
||||||
|
confirmText: 'Ja',
|
||||||
|
confirm: () => callback(true),
|
||||||
|
cancelText: 'Nein',
|
||||||
|
cancel: () => callback(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
return data
|
||||||
|
}
|
21
src/composables/useWishlistStore.ts
Normal file
21
src/composables/useWishlistStore.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import apiService from '@/services/apiService'
|
||||||
|
import { Wishlist, WishlistItem } from '@/types'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
const apiClient = apiService.getClient()
|
||||||
|
|
||||||
|
const getBySlugUrl = async (slugText: string): Promise<Wishlist> => {
|
||||||
|
const { data } = await apiClient.get(`/wishlist/${slugText}`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateItem = async (item: WishlistItem): Promise<void> => {
|
||||||
|
await apiClient.put(`/wishlist/${item.wishlistId}/item/${item.id}`, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWishlistStore = async (slugText: string) => {
|
||||||
|
const list = reactive(await getBySlugUrl(slugText))
|
||||||
|
return reactive({
|
||||||
|
list,
|
||||||
|
updateItem,
|
||||||
|
})
|
||||||
|
}
|
17
src/composables/useWishlistsStore.ts
Normal file
17
src/composables/useWishlistsStore.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import apiService from '@/services/apiService'
|
||||||
|
import { Wishlist } from '@/types'
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
const apiClient = apiService.getClient()
|
||||||
|
const prefix = '/wishlist'
|
||||||
|
|
||||||
|
export const getAll = async (): Promise<Wishlist[]> => {
|
||||||
|
const { data } = await apiClient.get(prefix)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWishlistsStore = async () => {
|
||||||
|
const lists = reactive(await getAll())
|
||||||
|
return reactive({
|
||||||
|
lists,
|
||||||
|
})
|
||||||
|
}
|
7
src/config/apiConfig.ts
Normal file
7
src/config/apiConfig.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
interface apiConfig {
|
||||||
|
baseURL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default <apiConfig>{
|
||||||
|
baseURL: import.meta.env.VITE_API_BASEURL || '/api',
|
||||||
|
}
|
1
src/config/index.ts
Normal file
1
src/config/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as apiConfig } from './apiConfig'
|
13
src/main.ts
Normal file
13
src/main.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import './assets/tailwind.css'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import Modal from '@/components/Modal.vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.component('modalOverlay', Modal)
|
||||||
|
|
||||||
|
app.mount('#app')
|
26
src/router/index.ts
Normal file
26
src/router/index.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
import DetailView from '../views/DetailView.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:slug',
|
||||||
|
name: 'detail',
|
||||||
|
component: DetailView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'notFound',
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
component: () => import('../views/NotFound.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
14
src/services/apiService.ts
Normal file
14
src/services/apiService.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||||
|
import { apiConfig } from '@/config'
|
||||||
|
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
baseURL: apiConfig.baseURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
const client: AxiosInstance = axios.create(config)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getClient: () => {
|
||||||
|
return client
|
||||||
|
},
|
||||||
|
}
|
22
src/types.ts
Normal file
22
src/types.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export interface WishlistItem {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
image: string
|
||||||
|
description: string
|
||||||
|
comment: string
|
||||||
|
bought: boolean
|
||||||
|
wishlistId: boolean
|
||||||
|
}
|
||||||
|
export interface Wishlist {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
imageSrc: string
|
||||||
|
slugUrlText: string
|
||||||
|
items: WishlistItem[]
|
||||||
|
}
|
||||||
|
export interface TileProp {
|
||||||
|
title: string
|
||||||
|
imageSrc: string
|
||||||
|
}
|
55
src/views/DetailView.vue
Normal file
55
src/views/DetailView.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { WishlistItem as WishlistItemType } from '@/types'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import Tile from '@/components/Tile.vue'
|
||||||
|
import WishlistItem from '@/components/WishlistItem.vue'
|
||||||
|
import { useWishlistStore, useModal } from '@/composables'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const modal = useModal()
|
||||||
|
const wishlistStore = await useWishlistStore(route.params.slug as string)
|
||||||
|
const list = wishlistStore.list
|
||||||
|
const notBoughtItems = computed(() => {
|
||||||
|
return list.items.filter((item: WishlistItemType) => item.bought === false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const bought = async (item: WishlistItemType): Promise<void> => {
|
||||||
|
const confirmed = await modal.show(
|
||||||
|
'Möchten Sie den Gegenstand von der Liste nehmen?',
|
||||||
|
'Durch das das runternehmen von der Liste ist dieser Gegenstand nicht mehr andere sichtbar.'
|
||||||
|
)
|
||||||
|
if (confirmed) {
|
||||||
|
item.bought = true
|
||||||
|
wishlistStore.updateItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col md:flex-row space-x-0 md:space-x-6 space-y-2 md:space-y-0 items-center"
|
||||||
|
>
|
||||||
|
<Tile :image-src="list.imageSrc" class="shrink-0"></Tile>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-center md:text-left mb-2">
|
||||||
|
{{ list.title }}
|
||||||
|
</h1>
|
||||||
|
<p v-if="list.description" class="text-lg">
|
||||||
|
{{ list.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-14 md:space-y-8 my-10">
|
||||||
|
<WishlistItem
|
||||||
|
v-for="(item, index) in notBoughtItems"
|
||||||
|
:key="index"
|
||||||
|
:title="item.title"
|
||||||
|
:url="item.url"
|
||||||
|
:image="item.image"
|
||||||
|
:description="item.description"
|
||||||
|
:comment="item.comment"
|
||||||
|
@bought="bought(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
23
src/views/HomeView.vue
Normal file
23
src/views/HomeView.vue
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Tile from '@/components/Tile.vue'
|
||||||
|
import { useWishlistsStore } from '@/composables'
|
||||||
|
const wishlistStore = await useWishlistsStore()
|
||||||
|
const lists = wishlistStore.lists
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="text-3xl text-center">Wunschlisten</h1>
|
||||||
|
<div class="flex flex-row flex-wrap justify-around p-10">
|
||||||
|
<router-link
|
||||||
|
v-for="(item, index) in lists"
|
||||||
|
:key="index"
|
||||||
|
:to="'/' + item.slugUrlText"
|
||||||
|
>
|
||||||
|
<Tile
|
||||||
|
:title="item.title"
|
||||||
|
:image-src="item.imageSrc"
|
||||||
|
class="m-2 hover:ring-2 ring-slate-500"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
3
src/views/NotFound.vue
Normal file
3
src/views/NotFound.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<h1>Oops, it looks like the page you're looking for doesn't exist.</h1>
|
||||||
|
</template>
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
module.exports = {
|
||||||
|
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
7
tsconfig.backend.json
Normal file
7
tsconfig.backend.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs"
|
||||||
|
},
|
||||||
|
"include": ["src/api/**/*"]
|
||||||
|
}
|
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"target": "esnext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["vite.config.*", "env.d.ts", "src/**/*", "src/**/*.vue"]
|
||||||
|
}
|
17
vite.config.ts
Normal file
17
vite.config.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { fileURLToPath, URL } from '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)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist/static',
|
||||||
|
},
|
||||||
|
})
|
Loading…
Add table
Reference in a new issue