Added Docker
This commit is contained in:
@@ -1,4 +1,13 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.nuxt
|
npm-debug.log
|
||||||
dist
|
package-lock.json
|
||||||
.git
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
.nyc_output
|
||||||
|
coverage
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -10,8 +10,7 @@ dist
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
*.log*
|
||||||
*.log
|
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -22,3 +21,5 @@ logs
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
8
.sequelizerc
Normal file
8
.sequelizerc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'config': path.resolve('server/database', 'config.js'),
|
||||||
|
'models-path': path.resolve('server/database', 'models'),
|
||||||
|
'seeders-path': path.resolve('server/database', 'seeders'),
|
||||||
|
'migrations-path': path.resolve('server/database', 'migrations')
|
||||||
|
};
|
||||||
14
Dockerfile
14
Dockerfile
@@ -1,12 +1,12 @@
|
|||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache git bash
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
|
||||||
COPY entrypoint.sh /usr/src/app/entrypoint.sh
|
COPY entrypoint.sh /app/entrypoint.sh
|
||||||
RUN chmod +x /usr/src/app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 1337
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
|
|
||||||
175
README.md
175
README.md
@@ -1,75 +1,144 @@
|
|||||||
# Nuxt UI Starter
|
# Nuxt 4 met Database
|
||||||
|
|
||||||
Look at [Nuxt docs](https://nuxt.com/docs/getting-started/introduction) and [Nuxt UI docs](https://ui.nuxt.com) to learn more.
|
Een moderne Nuxt 4 applicatie met PostgreSQL database, draaiend via Docker Compose.
|
||||||
|
|
||||||
## Setup
|
## Features
|
||||||
|
|
||||||
Make sure to install the dependencies:
|
- 🚀 Nuxt 4 met TypeScript
|
||||||
|
- 🐘 PostgreSQL database
|
||||||
|
- 🐳 Docker & Docker Compose
|
||||||
|
- 🎨 Tailwind CSS voor styling
|
||||||
|
- 📊 Sequelize ORM voor database management
|
||||||
|
- 🔄 API routes voor CRUD operaties
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Clone en setup
|
||||||
```bash
|
```bash
|
||||||
# npm
|
git clone <repository-url>
|
||||||
|
cd nuxt-deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start met Docker Compose
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Database migratie (eerste keer)
|
||||||
|
```bash
|
||||||
|
# In een nieuwe terminal
|
||||||
|
docker-compose exec nuxt-app npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Open de applicatie
|
||||||
|
Ga naar [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Lokale development (zonder Docker)
|
||||||
|
```bash
|
||||||
|
# Installeer dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# pnpm
|
# Start PostgreSQL (via Docker)
|
||||||
pnpm install
|
docker-compose up postgres -d
|
||||||
|
|
||||||
# yarn
|
# Setup database
|
||||||
yarn install
|
npm run db:migrate
|
||||||
|
|
||||||
# bun
|
# Start development server
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Server
|
|
||||||
|
|
||||||
Start the development server on `http://localhost:3000`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm run dev
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn dev
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production
|
### Database management
|
||||||
|
```bash
|
||||||
|
# Run migrations
|
||||||
|
npm run db:migrate
|
||||||
|
|
||||||
Build the application for production:
|
# Create database
|
||||||
|
npm run db:create
|
||||||
|
|
||||||
|
# Seed database
|
||||||
|
npm run db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Users
|
||||||
|
- `GET /api/users` - Alle gebruikers ophalen
|
||||||
|
- `POST /api/users` - Nieuwe gebruiker aanmaken
|
||||||
|
|
||||||
|
### Posts
|
||||||
|
- `GET /api/posts` - Alle posts ophalen
|
||||||
|
- `POST /api/posts` - Nieuwe post aanmaken
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### User
|
||||||
|
- `id` (Int, Primary Key)
|
||||||
|
- `email` (String, Unique)
|
||||||
|
- `name` (String, Optional)
|
||||||
|
- `createdAt` (DateTime)
|
||||||
|
- `updatedAt` (DateTime)
|
||||||
|
|
||||||
|
### Post
|
||||||
|
- `id` (Int, Primary Key)
|
||||||
|
- `title` (String)
|
||||||
|
- `content` (String, Optional)
|
||||||
|
- `published` (Boolean, Default: false)
|
||||||
|
- `authorId` (Int, Foreign Key)
|
||||||
|
- `createdAt` (DateTime)
|
||||||
|
- `updatedAt` (DateTime)
|
||||||
|
|
||||||
|
## Docker Services
|
||||||
|
|
||||||
|
- **nuxt-app**: Nuxt 4 applicatie (poort 3000)
|
||||||
|
- **postgres**: PostgreSQL database (poort 5432)
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Kopieer `env.example` naar `.env` en pas aan indien nodig:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# npm
|
cp env.example .env
|
||||||
npm run build
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn build
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Locally preview production build:
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Database connection issues
|
||||||
```bash
|
```bash
|
||||||
# npm
|
# Check of PostgreSQL draait
|
||||||
npm run preview
|
docker-compose ps
|
||||||
|
|
||||||
# pnpm
|
# Check logs
|
||||||
pnpm run preview
|
docker-compose logs postgres
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn preview
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run preview
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
### Sequelize issues
|
||||||
|
```bash
|
||||||
|
# Run migrations
|
||||||
|
docker-compose exec nuxt-app npm run db:migrate
|
||||||
|
|
||||||
|
# Reset en herstart database
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
Voor productie deployment:
|
||||||
|
|
||||||
|
1. Update `docker-compose.yml` met productie instellingen
|
||||||
|
2. Gebruik environment variables voor secrets
|
||||||
|
3. Setup reverse proxy (nginx)
|
||||||
|
4. Configure SSL certificaten
|
||||||
|
5. Setup database backups
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Frontend**: Nuxt 4, Vue 3, TypeScript
|
||||||
|
- **Backend**: Nuxt Server API
|
||||||
|
- **Database**: PostgreSQL 15
|
||||||
|
- **ORM**: Sequelize
|
||||||
|
- **Styling**: Tailwind CSS
|
||||||
|
- **Containerization**: Docker & Docker Compose
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export default defineAppConfig({
|
|
||||||
// https://ui.nuxt.com/getting-started/theme#design-system
|
|
||||||
ui: {
|
|
||||||
colors: {
|
|
||||||
primary: 'emerald',
|
|
||||||
neutral: 'slate',
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
defaultVariants: {
|
|
||||||
// Set default button color to neutral
|
|
||||||
// color: 'neutral'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UApp>
|
|
||||||
<NuxtPage />
|
|
||||||
</UApp>
|
|
||||||
</template>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@import "@nuxt/ui";
|
|
||||||
|
|
||||||
@theme static {
|
|
||||||
--font-sans: 'Public Sans', sans-serif;
|
|
||||||
|
|
||||||
--color-green-50: #effdf5;
|
|
||||||
--color-green-100: #d9fbe8;
|
|
||||||
--color-green-200: #b3f5d1;
|
|
||||||
--color-green-300: #75edae;
|
|
||||||
--color-green-400: #00dc82;
|
|
||||||
--color-green-500: #00c16a;
|
|
||||||
--color-green-600: #00a155;
|
|
||||||
--color-green-700: #007f45;
|
|
||||||
--color-green-800: #016538;
|
|
||||||
--color-green-900: #0a5331;
|
|
||||||
--color-green-950: #052e16;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col items-center justify-center gap-4 h-screen">
|
|
||||||
<h1 class="font-bold text-2xl text-(--ui-primary)">
|
|
||||||
Nuxt UI - Starter
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UButton
|
|
||||||
label="Documentation"
|
|
||||||
icon="i-lucide-square-play"
|
|
||||||
to="https://ui.nuxt.com/getting-started/installation/nuxt"
|
|
||||||
target="_blank"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
label="GitHub"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
icon="i-simple-icons-github"
|
|
||||||
to="https://github.com/nuxt/ui"
|
|
||||||
target="_blank"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,28 +1,43 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nuxt-app:
|
app:
|
||||||
|
container_name: node-nuxt-4
|
||||||
build: .
|
build: .
|
||||||
container_name: nuxt-app
|
ports:
|
||||||
restart: always
|
- "1337:3000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=${NODE_ENV}
|
||||||
- GIT_REPO_URL=${GIT_REPO_URL}
|
- DB_HOST=mariadb
|
||||||
- GIT_BRANCH=${GIT_BRANCH}
|
- DB_PORT=3306
|
||||||
volumes:
|
- DB_NAME=${DB_NAME}
|
||||||
- nuxt-app-data:/app
|
- DB_USERNAME=${DB_USERNAME}
|
||||||
labels:
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
- "traefik.enable=true"
|
command: ["sh", "/app/entrypoint.sh"]
|
||||||
- "traefik.http.routers.nuxt-app.rule=Host(`app.tiemen.dev`)"
|
restart: unless-stopped
|
||||||
- "traefik.http.routers.nuxt-app.entrypoints=websecure"
|
depends_on:
|
||||||
- "traefik.http.routers.nuxt-app.tls.certresolver=myresolver"
|
- mariadb
|
||||||
- "traefik.http.services.nuxt-app.loadbalancer.server.port=3000"
|
|
||||||
networks:
|
networks:
|
||||||
- web
|
- mariadb_network
|
||||||
|
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:latest
|
||||||
|
container_name: mariadb-nuxt-4
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MYSQL_DATABASE: ${DB_NAME}
|
||||||
|
MYSQL_USER: ${DB_USERNAME}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- mariadb-data:/var/lib/mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- mariadb_network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
nuxt-app-data:
|
mariadb-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
mariadb_network:
|
||||||
external: true
|
driver: bridge
|
||||||
@@ -1,26 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
set -e
|
|
||||||
|
|
||||||
TEMP_DIR="/tmp/nuxt_repo"
|
# Genereer SSL-certificaten als ze niet bestaan
|
||||||
|
if [ ! -f /app/server.crt ] || [ ! -f /app/server.key ]; then
|
||||||
# Als repo nog niet bestaat, clone naar tijdelijke map
|
openssl req -newkey rsa:2048 -nodes -keyout /app/server.key -x509 -days 365 -out /app/server.crt -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=example.com"
|
||||||
if [ ! -d ".git" ]; then
|
|
||||||
echo "Cloning repo to temporary folder..."
|
|
||||||
git clone -b ${GIT_BRANCH:-main} ${GIT_REPO_URL} $TEMP_DIR
|
|
||||||
echo "Copying files to /app..."
|
|
||||||
cp -R $TEMP_DIR/* $TEMP_DIR/.[!.]* /app/ 2>/dev/null || true
|
|
||||||
else
|
|
||||||
echo "Pulling latest changes..."
|
|
||||||
git reset --hard
|
|
||||||
git clean -fd
|
|
||||||
git pull origin ${GIT_BRANCH:-main}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Dependencies installeren
|
# Start de applicatie
|
||||||
npm ci
|
if [ "$NODE_ENV" = "production" ]; then
|
||||||
|
npm run build && npm run preview
|
||||||
# Build Nuxt
|
else
|
||||||
npm run build
|
npm install &&
|
||||||
|
npm run dev
|
||||||
# Start Nuxt
|
fi
|
||||||
exec npm run start
|
|
||||||
|
|||||||
5
env.example
Normal file
5
env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://nuxtuser:nuxtpassword@localhost:5432/nuxtdb"
|
||||||
|
|
||||||
|
# Nuxt
|
||||||
|
NUXT_PUBLIC_API_BASE="/api"
|
||||||
@@ -1,13 +1,27 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxt/ui',
|
'@nuxtjs/tailwindcss'
|
||||||
'@nuxt/eslint'
|
|
||||||
],
|
],
|
||||||
|
runtimeConfig: {
|
||||||
css: ['~/assets/css/main.css'],
|
// Private keys (only available on server-side)
|
||||||
|
database: process.env.DB_NAME || "mydatabase",
|
||||||
compatibilityDate: '2025-07-16'
|
username: process.env.DB_USERNAME || "root",
|
||||||
|
password: process.env.DB_PASSWORD || "",
|
||||||
|
host: process.env.DB_HOST || "localhost",
|
||||||
|
dialect: process.env.DB_DIALECT || "mysql", // Change to "postgres", "sqlite", etc.
|
||||||
|
logging: process.env.DB_LOGGING === "true",
|
||||||
|
// Public keys (exposed to client-side)
|
||||||
|
public: {
|
||||||
|
apiBase: '/api'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vite: {
|
||||||
|
},
|
||||||
|
nitro: {
|
||||||
|
experimental: {
|
||||||
|
wasm: true
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
9909
package-lock.json
generated
Normal file
9909
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -1,25 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "nuxt-app",
|
"name": "nuxt-deploy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Nuxt 4 application with database",
|
||||||
|
"type": "commonjs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"db:migrate": "npx sequelize-cli db:migrate",
|
||||||
"lint": "eslint .",
|
"db:seed": "npx sequelize-cli db:seed:all",
|
||||||
"lint:fix": "eslint --fix ."
|
"db:create": "npx sequelize-cli db:create"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@iconify-json/lucide": "^1.2.64",
|
|
||||||
"@iconify-json/simple-icons": "^1.2.49",
|
|
||||||
"@nuxt/ui": "^3.3.2",
|
|
||||||
"nuxt": "^4.0.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/eslint": "^1.9.0",
|
"@nuxt/devtools": "latest",
|
||||||
"eslint": "^9.34.0",
|
"nuxt": "^3.8.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.4.0",
|
||||||
|
"sequelize-cli": "^6.6.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||||
|
"sequelize": "^6.37.0",
|
||||||
|
"pg": "^8.12.0",
|
||||||
|
"pg-hstore": "^2.3.4",
|
||||||
|
"mariadb": "^3.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
169
pages/index.vue
Normal file
169
pages/index.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-4xl font-bold text-center mb-8 text-gray-800">
|
||||||
|
Nuxt 4 met Database
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<!-- Users Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4 text-gray-700">Gebruikers</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="createUser" class="mb-4">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<input
|
||||||
|
v-model="newUser.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Naam"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="newUser.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
Gebruiker Toevoegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
class="p-3 bg-gray-50 rounded-md"
|
||||||
|
>
|
||||||
|
<h3 class="font-medium">{{ user.name }}</h3>
|
||||||
|
<p class="text-sm text-gray-600">{{ user.email }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ user.posts.length }} posts</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Posts Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4 text-gray-700">Posts</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="createPost" class="mb-4">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<input
|
||||||
|
v-model="newPost.title"
|
||||||
|
type="text"
|
||||||
|
placeholder="Titel"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-model="newPost.content"
|
||||||
|
placeholder="Inhoud"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<select
|
||||||
|
v-model="newPost.authorId"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Selecteer auteur</option>
|
||||||
|
<option v-for="user in users" :key="user.id" :value="user.id">
|
||||||
|
{{ user.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full bg-green-500 text-white py-2 px-4 rounded-md hover:bg-green-600 transition-colors"
|
||||||
|
>
|
||||||
|
Post Toevoegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="post in posts"
|
||||||
|
:key="post.id"
|
||||||
|
class="p-4 bg-gray-50 rounded-md"
|
||||||
|
>
|
||||||
|
<h3 class="font-medium text-lg">{{ post.title }}</h3>
|
||||||
|
<p class="text-gray-600 mt-1">{{ post.content }}</p>
|
||||||
|
<div class="flex justify-between items-center mt-2 text-sm text-gray-500">
|
||||||
|
<span>Door: {{ post.author.name }}</span>
|
||||||
|
<span>{{ formatDate(post.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// Reactive data
|
||||||
|
const users = ref([])
|
||||||
|
const posts = ref([])
|
||||||
|
const newUser = ref({ name: '', email: '' })
|
||||||
|
const newPost = ref({ title: '', content: '', authorId: '' })
|
||||||
|
|
||||||
|
// Fetch data on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([fetchUsers(), fetchPosts()])
|
||||||
|
})
|
||||||
|
|
||||||
|
// API functions
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const data = await $fetch('/api/users')
|
||||||
|
users.value = data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching users:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPosts = async () => {
|
||||||
|
try {
|
||||||
|
const data = await $fetch('/api/posts')
|
||||||
|
posts.value = data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching posts:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUser = async () => {
|
||||||
|
try {
|
||||||
|
await $fetch('/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
body: newUser.value
|
||||||
|
})
|
||||||
|
newUser.value = { name: '', email: '' }
|
||||||
|
await fetchUsers()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating user:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPost = async () => {
|
||||||
|
try {
|
||||||
|
await $fetch('/api/posts', {
|
||||||
|
method: 'POST',
|
||||||
|
body: newPost.value
|
||||||
|
})
|
||||||
|
newPost.value = { title: '', content: '', authorId: '' }
|
||||||
|
await fetchPosts()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating post:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('nl-NL')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
16
server/api/db/migrate.post.ts
Normal file
16
server/api/db/migrate.post.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { sequelize } from '~/server/database'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
// Import and run migrations
|
||||||
|
const { execSync } = await import('child_process')
|
||||||
|
execSync('npx sequelize-cli db:migrate', { stdio: 'inherit' })
|
||||||
|
|
||||||
|
return { success: true, message: 'Database migrated successfully' }
|
||||||
|
} catch (error) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to migrate database'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
20
server/api/posts/index.get.ts
Normal file
20
server/api/posts/index.get.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Post, User } from '~/server/database/models'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const posts = await Post.findAll({
|
||||||
|
include: [{
|
||||||
|
model: User,
|
||||||
|
as: 'author'
|
||||||
|
}],
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
})
|
||||||
|
|
||||||
|
return posts
|
||||||
|
} catch (error) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to fetch posts'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
29
server/api/posts/index.post.ts
Normal file
29
server/api/posts/index.post.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Post, User } from '~/server/database/models'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const body = await readBody(event)
|
||||||
|
|
||||||
|
const post = await Post.create({
|
||||||
|
title: body.title,
|
||||||
|
content: body.content,
|
||||||
|
published: body.published || false,
|
||||||
|
authorId: body.authorId
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the post with author information
|
||||||
|
const postWithAuthor = await Post.findByPk(post.id, {
|
||||||
|
include: [{
|
||||||
|
model: User,
|
||||||
|
as: 'author'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
return postWithAuthor
|
||||||
|
} catch (error) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to create post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
19
server/api/users/index.get.ts
Normal file
19
server/api/users/index.get.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { User, Post } from '~/server/database/models';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const users = await User.findAll({
|
||||||
|
include: [{
|
||||||
|
model: Post,
|
||||||
|
as: 'posts'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
return users
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching users:', error);
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to fetch users'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
19
server/api/users/index.post.ts
Normal file
19
server/api/users/index.post.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { User } from '~/server/database/models'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const body = await readBody(event)
|
||||||
|
|
||||||
|
const user = await User.create({
|
||||||
|
email: body.email,
|
||||||
|
name: body.name
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
} catch (error) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to create user'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
28
server/database/config.js
Normal file
28
server/database/config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
development: {
|
||||||
|
username: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
dialect: 'mariadb'
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
username: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
dialect: 'mariadb'
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
username: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
dialect: 'mariadb'
|
||||||
|
}
|
||||||
|
};
|
||||||
11
server/database/index.js
Normal file
11
server/database/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
|
// Initialize Sequelize instance
|
||||||
|
const sequelize = new Sequelize(config.database, config.username, config.password, {
|
||||||
|
host: config.host,
|
||||||
|
dialect: config.dialect,
|
||||||
|
logging: config.logging || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { sequelize };
|
||||||
35
server/database/migrations/20240101000001-create-users.js
Normal file
35
server/database/migrations/20240101000001-create-users.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.createTable('users', {
|
||||||
|
id: {
|
||||||
|
allowNull: false,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
type: Sequelize.INTEGER
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
allowNull: false,
|
||||||
|
type: Sequelize.DATE
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
allowNull: false,
|
||||||
|
type: Sequelize.DATE
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.dropTable('users');
|
||||||
|
}
|
||||||
|
};
|
||||||
46
server/database/migrations/20240101000002-create-posts.js
Normal file
46
server/database/migrations/20240101000002-create-posts.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.createTable('posts', {
|
||||||
|
id: {
|
||||||
|
allowNull: false,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
type: Sequelize.INTEGER
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
published: {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
authorId: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
allowNull: false,
|
||||||
|
type: Sequelize.DATE
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
allowNull: false,
|
||||||
|
type: Sequelize.DATE
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.dropTable('posts');
|
||||||
|
}
|
||||||
|
};
|
||||||
37
server/database/models/Post.js
Normal file
37
server/database/models/Post.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../index.js';
|
||||||
|
|
||||||
|
const Post = sequelize.define('Post', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
published: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
authorId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'posts',
|
||||||
|
timestamps: true,
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Post;
|
||||||
26
server/database/models/User.js
Normal file
26
server/database/models/User.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../index.js';
|
||||||
|
|
||||||
|
const User = sequelize.define('User', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'users',
|
||||||
|
timestamps: true,
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default User;
|
||||||
8
server/database/models/index.js
Normal file
8
server/database/models/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import User from './User.js';
|
||||||
|
import Post from './Post.js';
|
||||||
|
|
||||||
|
// Define associations
|
||||||
|
User.hasMany(Post, { foreignKey: 'authorId', as: 'posts' });
|
||||||
|
Post.belongsTo(User, { foreignKey: 'authorId', as: 'author' });
|
||||||
|
|
||||||
|
export { User, Post };
|
||||||
Reference in New Issue
Block a user