1
0
Fork 0

chore: wipe

This commit is contained in:
Vojtěch Mareš 2023-11-01 21:40:35 +01:00
parent a7b06f1d6e
commit 4bc8d2d79f
Signed by: vojtech.mares
GPG key ID: C6827B976F17240D
58 changed files with 0 additions and 8571 deletions

View file

@ -1,11 +0,0 @@
.env
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
README.create-t3-gg.md
charts
seed.cjs

View file

@ -1,26 +0,0 @@
# Since the ".env" file is gitignored, you can use the ".env.example" file to
# build a new ".env" file when you clone the repo. Keep this file up-to-date
# when you add new variables to `.env`.
# This file will be committed to version control, so make sure not to have any
# secrets in it. If you are cloning this repo, create a copy of this file named
# ".env" and populate it with your secrets.
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DATABASE_URL="postgresql://username:password@localhost:5432/database"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"
# Next Auth Keycloak Provider
KEYCLOAK_CLIENT_ID=""
KEYCLOAK_CLIENT_SECRET=""
KEYCLOAK_ISSUER=""

View file

@ -1,35 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require("path");
/** @type {import("eslint").Linter.Config} */
const config = {
overrides: [
{
extends: [
"plugin:@typescript-eslint/recommended-requiring-type-checking",
],
files: ["*.ts", "*.tsx"],
parserOptions: {
project: path.join(__dirname, "tsconfig.json"),
},
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
project: path.join(__dirname, "tsconfig.json"),
},
plugins: ["@typescript-eslint"],
extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
rules: {
"@typescript-eslint/consistent-type-imports": [
"warn",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
},
};
module.exports = config;

44
.gitignore vendored
View file

@ -1,44 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
seed.cjs

View file

@ -1,137 +0,0 @@
default:
image: ghcr.io/vojtechmares/toolkit:latest
stages:
- lint
- build
- deploy:dry-run
- deploy
lint next.js:
stage: lint
image: node:18-alpine3.17
script:
- npm ci --frozen-lockfile
- SKIP_ENV_VALIDATION=1 npm run lint
lint helm:
stage: lint
script:
- helm lint ./charts/backoffice -f ./charts/backoffice/values.dummy.yaml --quiet
docker build:
stage: build
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
- docker info
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
deploy to staging (dry-run):
stage: deploy:dry-run
script:
- >
helm \
upgrade \
--install \
--atomic \
--wait=true \
--wait-for-jobs=true \
--timeout=900s \
--dry-run=true \
--namespace backoffice-staging \
--values ./charts/backoffice/values.staging.yaml \
--set image.tag=$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA \
--set dockerconfigjsonBase64=dummy \
--set backoffice.secrets.databaseURL=dummy \
--set backoffice.secrets.nextauthSecret=dummy \
--set backoffice.secrets.keycloakClientID=dummy \
--set backoffice.secrets.keycloakClientSecret=dummy \
--set backoffice.secrets.keycloakIssuer=dummy \
backoffice \
./charts/backoffice
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy to production (dry-run):
stage: deploy:dry-run
script:
- >
helm \
upgrade \
--install \
--atomic \
--wait=true \
--wait-for-jobs=true \
--timeout=900s \
--dry-run=true \
--namespace backoffice-production \
--values ./charts/backoffice/values.production.yaml \
--set image.tag=$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA \
--set dockerconfigjsonBase64=dummy \
--set backoffice.secrets.databaseURL=dummy \
--set backoffice.secrets.nextauthSecret=dummy \
--set backoffice.secrets.keycloakClientID=dummy \
--set backoffice.secrets.keycloakClientSecret=dummy \
--set backoffice.secrets.keycloakIssuer=dummy \
backoffice \
./charts/backoffice
rules:
- if: $CI_COMMIT_BRANCH == "production"
deploy to staging:
stage: deploy
script:
- >
helm \
upgrade \
--install \
--atomic \
--wait=true \
--wait-for-jobs=true \
--timeout=900s \
--namespace backoffice-staging \
--values ./charts/backoffice/values.staging.yaml \
--set image.tag=$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA \
--set dockerconfigjsonBase64=$DOCKERCONFIG_BASE64 \
--set backoffice.secrets.databaseURL=$DATABASE_URL \
--set backoffice.secrets.nextauthSecret=$NEXTAUTH_SECRET \
--set backoffice.secrets.keycloakClientID=$KEYCLOAK_CLIENT_ID \
--set backoffice.secrets.keycloakClientSecret=$KEYCLOAK_CLIENT_SECRET \
--set backoffice.secrets.keycloakIssuer=$KEYCLOAK_ISSUER \
backoffice \
./charts/backoffice
environment:
name: staging
url: https://staging.backoffice.mareshq.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy to production:
stage: deploy
script:
- >
helm \
upgrade \
--install \
--atomic \
--wait=true \
--wait-for-jobs=true \
--timeout=900s \
--namespace backoffice-production \
--values ./charts/backoffice/values.production.yaml \
--set image.tag=$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA \
--set dockerconfigjsonBase64=$DOCKERCONFIG_BASE64 \
--set backoffice.secrets.databaseURL=$DATABASE_URL \
--set backoffice.secrets.nextauthSecret=$NEXTAUTH_SECRET \
--set backoffice.secrets.keycloakClientID=$KEYCLOAK_CLIENT_ID \
--set backoffice.secrets.keycloakClientSecret=$KEYCLOAK_CLIENT_SECRET \
--set backoffice.secrets.keycloakIssuer=$KEYCLOAK_ISSUER \
backoffice \
./charts/backoffice
environment:
name: production
url: https://backoffice.mareshq.com
rules:
- if: $CI_COMMIT_BRANCH == "production"

View file

@ -1,65 +0,0 @@
FROM --platform=linux/amd64 node:18-alpine3.17 as base
### DEPENDENCIES
FROM base AS deps
WORKDIR /app
RUN apk add --no-cache libc6-compat openssl1.1-compat
COPY prisma ./prisma
COPY package.json package-lock.json ./
RUN npm ci --frozen-lockfile
### BUILDER
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/prisma ./prisma
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN SKIP_ENV_VALIDATION=1 npm run build
### RUNNER
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Required for `npx prisma migrate deploy`
COPY --from=builder /app/prisma ./prisma
# Required for `npx prisma db seed`
COPY --from=builder /app/content ./content
COPY --from=builder /app/src/content/training.ts ./src/content/training.ts
COPY --from=builder /app/src/server/db.ts ./src/server/db.ts
COPY --from=builder /app/src/env.mjs ./src/env.mjs
# Required for Next.js
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/tsconfig.json ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]

View file

@ -1,19 +0,0 @@
.PHONY: dev
dev:
npm run dev
.PHONY: db-push
db-push:
npx prisma db push
.PHONY: db-seed
db-seed:
npm run seed
.PHONY: db-migrate-dev
db-migrate-dev:
npx prisma migrate dev
.PHONY: lint
lint:
npm run lint

View file

@ -1,28 +0,0 @@
# Create T3 App
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
## What's next? How do I make an app with this?
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
- [Next.js](https://nextjs.org)
- [NextAuth.js](https://next-auth.js.org)
- [Prisma](https://prisma.io)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
## Learn More
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
- [Documentation](https://create.t3.gg/)
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
## How do I deploy this?
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.

View file

@ -1,7 +0,0 @@
# backoffice
A small application serving as a freelancer's backoffice.
A single source of truth. Currently an experiment.
The idea to have all training data source here and not as a part of a monorepo, is mostly laziness since I do not think monorepo tooling such as [Turborepo](https://turbo.build/repo) is quite ready and also it brings certain challenges with building etc. My target platform for this project is a small Kubernetes cluster.

View file

@ -1,6 +0,0 @@
apiVersion: v2
name: backoffice
description: A Kubernetes Helm chart for backoffice
type: application
version: 0.1.0
appVersion: 0.1.0

View file

@ -1,3 +0,0 @@
{{- if .Values.ingress.enabled }}
URL: https://{{ .Values.ingress.host }}
{{- end }}

View file

@ -1,11 +0,0 @@
kind: ConfigMap
apiVersion: v1
metadata:
name: {{ .Release.Name }}-config
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-15"
data:
NODE_ENV: {{ .Values.backoffice.env | quote }}
NEXTAUTH_URL: "https://{{ .Values.ingress.host }}/"
PORT: {{ .Values.service.portNumber | quote }}

View file

@ -1,70 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: backend
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: backend
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: backend
spec:
{{- if .Values.dockerconfigjsonBase64 }}
imagePullSecrets:
- name: {{ .Release.Name }}-container-registry
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: Always
ports:
- name: {{ .Values.service.port.name }}
containerPort: {{ .Values.service.port.number }}
protocol: TCP
livenessProbe:
httpGet:
path: /api/livez
port: {{ .Values.service.port.name }}
initialDelaySeconds: 3
periodSeconds: 3
timeoutSeconds: 2
failureThreshold: 3
sucessThreshold: 1
envFrom:
- configMapRef:
name: {{ .Release.Name }}-config
- secretRef:
name: {{ .Release.Name }}-database
- secretRef:
name: {{ .Release.Name }}-nextauth
resources:
{{- toYaml .Values.backend.resources | nindent 12 }}
terminationGracePeriodSeconds: 10
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/instance
operator: In
values:
- {{ .Release.Name }}
topologyKey: kubernetes.io/hostname

View file

@ -1,28 +0,0 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.ingressClassName }}
tls:
- hosts:
- {{ .Values.ingress.host | quote }}
secretName: {{ .Release.Name }}-tls
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: {{ .Values.ingress.path }}
pathType: {{ .Values.ingress.pathType }}
backend:
service:
name: {{ .Release.Name }}
port:
name: {{ .Values.service.port.name }}
{{- end -}}

View file

@ -1,36 +0,0 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migration
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": before-hook-creation #,hook-succeeded
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: database-migration
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 86400 # 1 day
activeDeadlineSeconds: 300 # 5 minutes
template:
spec:
{{- if .Values.dockerconfigjsonBase64 }}
imagePullSecrets:
- name: {{ .Release.Name }}-container-registry
{{- end }}
containers:
- name: {{ .Chart.Name }}-migration
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["npx", "prisma", "migrate", "deploy"]
envFrom:
- configMapRef:
name: {{ .Release.Name }}-config
- secretRef:
name: {{ .Release.Name }}-database
- secretRef:
name: {{ .Release.Name }}-nextauth
resources:
{{- toYaml .Values.migration.resources | nindent 10 }}
restartPolicy: Never

View file

@ -1,48 +0,0 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-seed
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation #,hook-succeeded
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: database-seed
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 86400 # 1 day
activeDeadlineSeconds: 300 # 5 minutes
template:
spec:
{{- if .Values.dockerconfigjsonBase64 }}
imagePullSecrets:
- name: {{ .Release.Name }}-container-registry
{{- end }}
containers:
- name: {{ .Chart.Name }}-seed
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["/bin/sh", "-c"]
args:
- |
npm install esbuild --no-save
npx esbuild prisma/seed.ts --outfile=/app/cache/seed.cjs --bundle --format=cjs --external:prisma --external:@prisma/client --platform=node
node /app/cache/seed.cjs
envFrom:
- configMapRef:
name: {{ .Release.Name }}-config
- secretRef:
name: {{ .Release.Name }}-database
- secretRef:
name: {{ .Release.Name }}-nextauth
volumeMounts:
- mountPath: /app/cache
name: cache-volume
resources:
{{- toYaml .Values.seed.resources | nindent 10 }}
restartPolicy: Never
volumes:
- name: cache-volume
emptyDir:
sizeLimit: 2Mi

View file

@ -1,13 +0,0 @@
{{- if gt (.Values.replicaCount | int) 1 }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ .Release.Name }}
spec:
minAvailable: {{ .Values.pdb.minAvailable }}
selector:
matchLabels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: backend
{{- end }}

View file

@ -1,12 +0,0 @@
{{ if .Values.dockerconfigjsonBase64 }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-container-registry
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-15"
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: {{ .Values.dockerconfigjsonBase64 }}
{{ end }}

View file

@ -1,10 +0,0 @@
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: {{ .Release.Name }}-database
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-15"
stringData:
DATABASE_URL: {{ .Values.backoffice.secrets.databaseURL | quote }}

View file

@ -1,13 +0,0 @@
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: {{ .Release.Name }}-nextauth
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-15"
stringData:
NEXTAUTH_SECRET: {{ .Values.backoffice.secrets.nextauthSecret | quote }}
KEYCLOAK_CLIENT_ID: {{ .Values.backoffice.secrets.keycloakClientID | quote }}
KEYCLOAK_CLIENT_SECRET: {{ .Values.backoffice.secrets.keycloakClientSecret | quote }}
KEYCLOAK_ISSUER: {{ .Values.backoffice.secrets.keycloakIssuer | quote }}

View file

@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port.number }}
targetPort: {{ .Values.service.port.number }}
protocol: TCP
name: {{ .Values.service.port.name }}
selector:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}

View file

@ -1,10 +0,0 @@
backoffice:
secretes:
databaseURL: "postgres://postgres:postgres@localhost:5432/backoffice"
nextauthSecret: "secret"
keycloakClientID: "secret"
keycloakClientSecret: "secret"
keycloakIssuer: "secret"
image:
tag: dummy

View file

@ -1,10 +0,0 @@
replicaCount: 2
ingress:
enabled: true
host: backoffice.mareshq.com
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
backoffice:
env: production

View file

@ -1,10 +0,0 @@
replicaCount: 2
ingress:
enabled: true
host: staging.backoffice.mareshq.com
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
backoffice:
env: production

View file

@ -1,63 +0,0 @@
replicaCount: 1
image:
repository: registry.mareshq.com/mareshq/backoffice
tag:
ingress:
enabled: false
host: example.com
ingressClassName: nginx
path: /
pathType: Prefix
annotations:
{}
# cert-manager.io/cluster-issuer: letsencrypt-prod
# cert-manager.io/issuer: letsencrypt-prod
service:
port:
name: http
number: 3000
pdb:
minAvailable: 1
backend:
resources:
limits:
cpu: 200m
memory: 512Mi
requests:
cpu: 200m
memory: 512Mi
migration:
resources:
limits:
cpu: 200m
memory: 512Mi
requests:
cpu: 200m
memory: 512Mi
seed:
resources:
limits:
cpu: 200m
memory: 512Mi
requests:
cpu: 200m
memory: 512Mi
backoffice:
env: null # allowed values: development | test | production
secrets:
databaseURL: null
nextauthSecret: null
keycloakClientID: null
keycloakClientSecret: null
keycloakIssuer: null
dockerconfigjsonBase64: null

View file

@ -1,45 +0,0 @@
---
id: s448cmvx05lra5mjyu35fdtd # generated via /scripts/cuid.mjs
name: Terraform
slug: terraform
days: 1
weight: 1
draft: false
logoURL: https://example.com/logo.png
svgIconURL: https://example.com/icon.svg
repositoryURL: https://github.com/vojtechmares/terraform-training
priceOpen: 5900
priceCorporate: 24000
---
# TODO
TEST ME
# DOES IT WORK?
LOOKS LIKE IT DOES!
# LETS CHECK OUT LISTS
- A
- B
# AND WHAT ABOUT OTHER STUFF?
- **STRONG**
- *ITALIC*
- [Link](https://google.com/)
## NESTED
### HEADINGS
OK WORK
> blockquote
```yaml
code:
block: true
```

View file

@ -1,25 +0,0 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
await import("./src/env.mjs");
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
/**
* If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config
* out.
*
* @see https://github.com/vercel/next.js/issues/41980
*/
i18n: {
locales: ["en"],
defaultLocale: "en",
},
swcMinify: true,
output: "standalone",
};
export default config;

6536
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,55 +0,0 @@
{
"name": "backoffice",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"postinstall": "prisma generate",
"lint": "next lint",
"start": "next start",
"seed": "esbuild prisma/seed.ts --outfile=seed.cjs --bundle --format=cjs --external:prisma --external:@prisma/client --platform=node && node seed.cjs"
},
"dependencies": {
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18",
"@next-auth/prisma-adapter": "^1.0.5",
"@paralleldrive/cuid2": "^2.2.1",
"@prisma/client": "^5.2.0",
"@t3-oss/env-nextjs": "^0.6.1",
"clsx": "^2.0.0",
"gray-matter": "^4.0.3",
"next": "^13.4.2",
"next-auth": "^4.23.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"zod": "^3.22.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/eslint": "^8.37.0",
"@types/node": "^18.17.15",
"@types/prettier": "^3.0.0",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"autoprefixer": "^10.4.14",
"esbuild": "^0.19.2",
"eslint": "^8.40.0",
"eslint-config-next": "^13.4.2",
"postcss": "^8.4.21",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"prisma": "^5.2.0",
"tailwindcss": "^3.3.0",
"typescript": "^5.2.2"
},
"ct3aMetadata": {
"initVersion": "7.14.1"
},
"engines": {
"node": ">=18.0.0 <19.0.0"
}
}

View file

@ -1,8 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
module.exports = config;

View file

@ -1,6 +0,0 @@
/** @type {import("prettier").Config} */
const config = {
plugins: [require.resolve("prettier-plugin-tailwindcss")],
};
module.exports = config;

View file

@ -1,66 +0,0 @@
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,23 +0,0 @@
-- CreateTable
CREATE TABLE "Training" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"days" INTEGER NOT NULL,
"weight" INTEGER NOT NULL,
"draft" BOOLEAN NOT NULL DEFAULT true,
"logoURL" TEXT,
"svgIconURL" TEXT,
"repositoryURL" TEXT,
"priceOpen" INTEGER NOT NULL,
"priceCorporate" INTEGER NOT NULL,
"content" TEXT,
CONSTRAINT "Training_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Training_slug_key" ON "Training"("slug");
-- CreateIndex
CREATE INDEX "Training_slug_idx" ON "Training"("slug");

View file

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View file

@ -1,79 +0,0 @@
// 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"
previewFeatures = ["jsonProtocol"]
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
// NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
// Further reading:
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
// https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
url = env("DATABASE_URL")
}
model Training {
id String @id @default(cuid())
name String
slug String @unique
days Int
weight Int
draft Boolean @default(true)
logoURL String?
svgIconURL String?
repositoryURL String?
priceOpen Int
priceCorporate Int
content String?
@@index([slug])
}
// Necessary for Next auth
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? // @db.Text
access_token String? // @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? // @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}

View file

@ -1,53 +0,0 @@
import { prisma } from "~/server/db";
import { getAllTrainingsWithMetadata } from "~/content/training";
async function seed() {
const trainings = await getAllTrainingsWithMetadata();
let instertedTrainings = [];
for (const training of trainings) {
instertedTrainings.push(
await prisma.training.upsert({
where: { id: training.metadata.id },
update: {
id: training.metadata.id ,
name: training.metadata.name,
slug: training.metadata.slug,
days: training.metadata.days,
weight: training.metadata.weight,
draft: training.metadata.draft,
logoURL: training.metadata.logoURL,
svgIconURL: training.metadata.svgIconURL,
repositoryURL: training.metadata.repositoryURL,
priceOpen: training.metadata.priceOpen,
priceCorporate: training.metadata.priceCorporate,
content: training.content,
},
create: {
id: training.metadata.id ,
name: training.metadata.name,
slug: training.metadata.slug,
days: training.metadata.days,
weight: training.metadata.weight,
draft: training.metadata.draft,
logoURL: training.metadata.logoURL,
svgIconURL: training.metadata.svgIconURL,
repositoryURL: training.metadata.repositoryURL,
priceOpen: training.metadata.priceOpen,
priceCorporate: training.metadata.priceCorporate,
content: training.content,
},
})
);
}
}
seed()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,5 +0,0 @@
#!/usr/bin/env node
import { createId } from '@paralleldrive/cuid2';
console.log(createId());

View file

@ -1,75 +0,0 @@
import clsx from "clsx";
import Link from "next/link";
import { type ReactNode } from "react";
const baseStyles = {
solid:
"group inline-flex items-center justify-center rounded-md font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2",
outline:
"group inline-flex ring-1 items-center justify-center rounded-md focus:outline-none",
};
const variantStyles = {
solid: {
black:
"bg-black text-white hover:bg-gray-700 active:bg-gray-800 focus-visible:outline-gray-900",
amber:
"bg-amber-500 text-white hover:bg-amber-600 active:bg-amber-800 focus-visible:outline-amber-500",
white:
"bg-white text-black hover:bg-amber-50 active:bg-amber-200 focus-visible:outline-white",
},
outline: {
black:
"ring-gray-200 text-black hover:ring-gray-300 active:bg-gray-100 focus-visible:outline-amber-500 focus-visible:ring-gray-300",
white:
"ring-gray-700 text-white hover:ring-gray-500 active:ring-gray-700 focus-visible:outline-white",
amber: "", // Outline buttons cannot be amber
},
};
const transitionStyle = "transition duration-150 ease-in-out";
const sizeStyles = {
medium: "px-4 py-2 text-sm",
large: "px-8 py-4 text-base",
};
type Props = {
variant?: "solid" | "outline";
color?: "black" | "white" | "amber";
size?: "medium" | "large";
className?: string;
href?: string;
children?: ReactNode;
};
export function Button({
variant = "solid",
color = "black",
size = "medium",
className,
href,
children,
}: Props) {
if (variant === "outline" && color === "amber") {
throw new Error("Outline buttons cannot be amber");
}
className = clsx(
baseStyles[variant],
variantStyles[variant][color],
sizeStyles[size],
transitionStyle,
className
);
if (href !== undefined) {
return (
<Link href={href} className={className}>
{children}
</Link>
);
} else {
return <button className={className}>{children}</button>;
}
}

View file

@ -1,18 +0,0 @@
import { Sidebar } from "~/components/Sidebar";
export type LayoutProps = {
children: React.ReactNode;
};
export function Layout({ children }: LayoutProps) {
return (
<>
<Sidebar />
<main className="py-10 lg:pl-72">
<div className="px-4 sm:px-6 lg:px-8">
{children}
</div>
</main>
</>
);
}

View file

@ -1,158 +0,0 @@
import clsx from "clsx";
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import {
Bars3Icon,
HomeIcon,
XMarkIcon,
AcademicCapIcon,
ArrowRightOnRectangleIcon,
} from '@heroicons/react/24/outline';
const navigation = [
{ name: 'Dashboard', href: '/', icon: HomeIcon },
{ name: 'Trainings', href: '/trainings', icon: AcademicCapIcon },
]
export function Sidebar() {
const [sidebarOpen, setSidebarOpen] = useState(false)
const router = useRouter();
return (
<>
<Transition.Root show={sidebarOpen} as={Fragment}>
<Dialog as="div" className="relative z-50 lg:hidden" onClose={setSidebarOpen}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/80" />
</Transition.Child>
<div className="fixed inset-0 flex">
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
<button type="button" className="-m-2.5 p-2.5" onClick={() => setSidebarOpen(false)}>
<span className="sr-only">Close sidebar</span>
<XMarkIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</Transition.Child>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-black px-6 pb-2 ring-1 ring-white/10">
<div className="flex h-16 shrink-0 items-center">
<span className="text-amber-600 text-2xl font-bold">backoffice</span>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<Link
href={item.href}
className={clsx(
router.pathname === item.href
? 'bg-gray-800 text-white'
: 'text-gray-400 hover:text-white hover:bg-gray-800',
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold'
)}
>
<item.icon className="h-6 w-6 shrink-0" aria-hidden="true" />
{item.name}
</Link>
</li>
))}
</ul>
</li>
</ul>
</nav>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-black px-6">
<div className="flex h-16 shrink-0 items-center">
<span className="text-amber-600 text-2xl font-bold">backoffice</span>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<Link
href={item.href}
className={clsx(
router.pathname === item.href
? 'bg-gray-800 text-white'
: 'text-gray-400 hover:text-white hover:bg-gray-800',
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold'
)}
>
<item.icon className="h-6 w-6 shrink-0" aria-hidden="true" />
{item.name}
</Link>
</li>
))}
</ul>
</li>
<li className="-mx-6 mt-auto">
<Link
href="/api/auth/signout"
className="flex items-center gap-x-4 px-6 py-3 text-sm font-semibold leading-6 text-white hover:bg-gray-800"
>
<ArrowRightOnRectangleIcon className="h-6 w-6" />
<span>Sign out</span>
</Link>
</li>
</ul>
</nav>
</div>
</div>
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-black px-4 py-4 shadow-sm sm:px-6 lg:hidden">
<button type="button" className="-m-2.5 p-2.5 text-gray-400 lg:hidden" onClick={() => setSidebarOpen(true)}>
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</button>
<div className="flex-1 text-sm font-semibold leading-6 text-white">Dashboard</div>
<Link href="/api/auth/signout">
<span className="sr-only">Sign out</span>
<ArrowRightOnRectangleIcon className="h-6 w-6 text-white" />
</Link>
</div>
</>
)
}

View file

@ -1,65 +0,0 @@
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
type Training = {
metadata: {
id: string;
name: string;
slug: string;
days: number;
weight: number;
draft?: boolean;
logoURL?: string;
svgIconURL?: string;
repositoryURL?: string;
priceOpen: number;
priceCorporate: number;
};
content: string;
};
export type { Training };
const root = process.cwd();
export function getTrainingFiles() {
return fs.readdirSync(path.join(root, 'content', 'training'), 'utf-8');
}
export function getTrainingBySlug(slug: string) {
const source = fs.readFileSync(path.join(root, 'content', 'training', `${slug}.md`), 'utf8');
const { data, content } = matter(source);
return {
metadata: data,
content: content,
};
}
export function getAllTrainingsWithMetadata(): Training[] {
const files = fs.readdirSync(path.join(root, 'content', 'training'))
const trainings = [] as Training[];
for (const fileName of files) {
const source = fs.readFileSync(path.join(root, 'content', 'training', fileName), 'utf8');
const { data: metadata, content: content } = matter(source);
trainings.push({metadata, content} as Training);
}
return trainings;
// return files.reduce((allTrainings, fileName) => {
// const source = fs.readFileSync(path.join(root, 'content', 'training', fileName), 'utf8');
// const training = matter(source);
// return [
// training,
// ...allTrainings,
// ]
// }, [])
}

View file

@ -1,9 +0,0 @@
export function formatCurrency(price: number | bigint, locale ='en-US', currency = 'CZK'): string {
const currencyFormatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
maximumFractionDigits: 0,
});
return currencyFormatter.format(price).replace(',', ' ')
}

View file

@ -1,56 +0,0 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars.
*/
server: {
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
NEXTAUTH_SECRET:
process.env.NODE_ENV === "production"
? z.string().min(1)
: z.string().min(1).optional(),
NEXTAUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
// Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string().min(1) : z.string().url(),
),
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
KEYCLOAK_CLIENT_ID: z.string(),
KEYCLOAK_CLIENT_SECRET: z.string(),
KEYCLOAK_ISSUER: z.string(),
},
/**
* Specify your client-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars. To expose them to the client, prefix them with
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
KEYCLOAK_CLIENT_ID: process.env.KEYCLOAK_CLIENT_ID,
KEYCLOAK_CLIENT_SECRET: process.env.KEYCLOAK_CLIENT_SECRET,
KEYCLOAK_ISSUER: process.env.KEYCLOAK_ISSUER,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds.
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
});

View file

@ -1,17 +0,0 @@
import { type Session } from "next-auth";
import { SessionProvider } from "next-auth/react";
import { type AppType } from "next/app";
import "~/styles/globals.css";
const MyApp: AppType<{ session: Session | null }> = ({
Component,
pageProps: { session, ...pageProps },
}) => {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
};
export default MyApp;

View file

@ -1,13 +0,0 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html className="h-full bg-white">
<Head />
<body className="h-full">
<Main />
<NextScript />
</body>
</Html>
)
}

View file

@ -1,4 +0,0 @@
import NextAuth from "next-auth";
import { authOptions } from "~/server/auth";
export default NextAuth(authOptions);

View file

@ -1,14 +0,0 @@
import type {NextApiRequest, NextApiResponse} from 'next'
// GET /api/livez
export default function handle(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== 'GET') {
res.status(405).end();
return;
}
res.status(200).json({status: 'ok'});
}

View file

@ -1,17 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '~/server/db';
// GET /api/v1/trainings/:id
export default async function handle(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== 'GET') return res.status(405);
const trainingId = req.query.id as string;
const training = await prisma.training.findUnique({ where: { id: trainingId }});
if (!training) return res.status(404).json({ message: 'Not found' });
return res.status(200).json(training)
}

View file

@ -1,13 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '~/server/db';
// GET /api/v1/trainings
export default async function handle(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== 'GET') return res.status(405);
const trainings = await prisma.training.findMany();
return res.status(200).json(trainings)
}

View file

@ -1,28 +0,0 @@
import Head from "next/head";
import { type GetServerSideProps } from "next";
import { getServerAuthSession } from "~/server/auth";
import { Layout } from "~/components/Layout";
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
if (!session) return { redirect: { destination: '/api/auth/signin', permanent: false } };
return { props: { session } };
}
export default function Home() {
return (
<>
<Head>
<title>Dashboard | MaresHQ backoffice</title>
<meta name="description" content="Generated by create-t3-app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Layout>
<div></div>
</Layout>
</>
);
}

View file

@ -1,107 +0,0 @@
import ReactMarkdown from 'react-markdown';
import { type GetServerSideProps } from "next";
import Head from "next/head";
import Link from "next/link";
import { formatCurrency } from "~/currency/formatter";
import { getServerAuthSession } from "~/server/auth";
import { prisma } from "~/server/db";
import { Layout } from "~/components/Layout";
type Training = {
id: string;
name: string;
slug: string;
days: number;
draft: boolean;
priceOpen: number;
priceCorporate: number;
logoURL: string;
svgIconURL: string;
repositoryURL: string;
content: string;
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
if (!session) return { redirect: { destination: '/api/auth/signin', permanent: false } };
const trainingSlug = ctx.query.slug as string;
const training = await prisma.training.findUnique({ where: { slug: trainingSlug }});
if (!training) return { notFound: true };
return { props: { training: training, session } };
}
function Detail({ training }: { training: Training }) {
return (
<div className="overflow-hidden bg-white shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<div className="px-4 py-6 sm:px-6">
<h3 className="text-base font-semibold leading-7 text-gray-900">Training</h3>
{/* <p className="mt-1 max-w-2xl text-sm leading-6 text-gray-500">Personal details and application.</p> */}
</div>
<div className="border-t border-gray-100">
<dl className="divide-y divide-gray-100">
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-900">backoffice ID</dt>
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><code>{training.id}</code></dd>
</div>
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-900">Name</dt>
<dd className="mt-1 text-sm leading-6 font-medium text-gray-900 sm:col-span-2 sm:mt-0">{training.name}</dd>
</div>
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-900">Days</dt>
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{training.days}</dd>
</div>
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-900">Draft</dt>
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{training.draft ? 'Yes' : 'No'}</dd>
</div>
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-900">Logo</dt>
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{training.logoURL}</dd>
</div>
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-900">Repository</dt>
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><Link href={training.repositoryURL} className="text-black underline">{training.repositoryURL}</Link></dd>
</div>
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-900">Price</dt>
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">Open:&nbsp;{formatCurrency(training.priceOpen)}, Corporate:&nbsp;{formatCurrency(training.priceCorporate)}</dd>
</div>
{/* <div className="px-4 py-6 sm:grid sm:grid-cols-2 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-900">Content</dt>
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-1 sm:mt-0">
<div className="prose prose:text-black prose-p:text-slate-700 prose-h1:text-2xl prose-h1:font-medium prose-h2:text-xl prose-h2:font-medium prose-h3:text-lg prose-h3:font-medium prose-li:my-0">
<ReactMarkdown>
{training.content}
</ReactMarkdown>
</div>
</dd>
</div> */}
<div className="mx-auto px-4 py-6 sm:gap-4 sm:px-6 prose prose:text-black prose-p:text-slate-700 prose-h1:text-2xl prose-h1:font-medium prose-h2:text-xl prose-h2:font-medium prose-h3:text-lg prose-h3:font-medium prose-li:my-0">
<ReactMarkdown>
{training.content}
</ReactMarkdown>
</div>
</dl>
</div>
</div>
)
}
export default function Training({ training }: { training: Training }) {
return (
<>
<Head>
<title>Training | MaresHQ backoffice</title>
</Head>
<Layout>
<Detail training={training} />
</Layout>
</>
);
}

View file

@ -1,133 +0,0 @@
import { type GetServerSideProps } from "next";
import Head from "next/head";
import Link from "next/link";
import { formatCurrency } from "~/currency/formatter";
import { getServerAuthSession } from "~/server/auth";
import { prisma } from "~/server/db";
import { Training } from "~/content/training";
import { Layout } from "~/components/Layout";
import { Button } from "~/components/Button";
type Trainings = [
{
id: string;
name: string;
slug: string;
days: number;
draft: boolean;
priceOpen: number;
priceCorporate: number;
}
];
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
if (!session) return { redirect: { destination: '/api/auth/signin', permanent: false } };
const trainings = await prisma.training.findMany({
select: {
id: true,
name: true,
slug: true,
days: true,
draft: true,
priceOpen: true,
priceCorporate: true,
}
});
return { props: { trainings: trainings, session } };
}
function Table({trainings}: { trainings: Trainings }) {
return (
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-base font-semibold leading-6 text-gray-900">Trainings</h1>
{/* <p className="mt-2 text-sm text-gray-700">
A list of all the users in your account including their name, title, email and role.
</p> */}
</div>
<div className="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
{/* <button
type="button"
className="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Add user
</button> */}
<Button href="https://github.com/vojtechmares/backoffice/new/main/content/training">
Add training
</Button>
</div>
</div>
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Name
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Days
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Draft
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Price Open
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Price Corporate
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{trainings.map((training) => (
<tr key={training.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
{training.name}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{training.days}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{training.draft ? 'Yes' : 'No'}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{formatCurrency(training.priceOpen)}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{formatCurrency(training.priceCorporate)}</td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<Link href={"/training/" + training.slug} className="text-black underline">
Detail<span className="sr-only"> of {training.name}</span>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
)
}
export default function Training({ trainings }: { trainings: Trainings }) {
return (
<>
<Head>
<title>Training | MaresHQ backoffice</title>
</Head>
<Layout>
{/* { JSON.stringify(trainings) } */}
<Table trainings={trainings} />
</Layout>
</>
);
}

View file

@ -1,147 +0,0 @@
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { type GetServerSidePropsContext } from "next";
import {
getServerSession,
type NextAuthOptions,
type DefaultSession,
} from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";
import { type AdapterAccount } from "next-auth/adapters";
import { type JWT } from "next-auth/jwt";
import { env } from "~/env.mjs";
import { prisma } from "~/server/db";
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
* object and keep type safety.
*
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
*/
declare module "next-auth" {
interface Session extends DefaultSession {
user: {
id: string;
// ...other properties
// role: UserRole;
} & DefaultSession["user"];
}
// interface User {
// // ...other properties
// // role: UserRole;
// }
}
/**
* Part of the Keycloak fix/workaround, see code bellow for method `signOut`.
*
* @see https://stackoverflow.com/a/75526977
*/
declare module "next-auth/jwt" {
interface JWT {
id_token?: string;
provider?: string;
}
}
const adapter = PrismaAdapter(prisma);
const originLinkAccount = adapter.linkAccount;
/**
* This method override handles Keycloak response with fields we are not expecting,
* as a part of the response and we have no database fields for them,
* which caused error on writing data to database.
*
* @see https://stackoverflow.com/questions/69910570/prisma-with-next-auth-user-creation-fails-cause-of-keycloaks-api-response-key
*/
adapter.linkAccount = (account: AdapterAccount) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { "not-before-policy": _, refresh_expires_in, ...data } = account;
if (!originLinkAccount) {
return;
}
return originLinkAccount(data);
};
/**
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
*
* @see https://next-auth.js.org/configuration/options
*/
export const authOptions: NextAuthOptions = {
callbacks: {
session: ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
email: user.email,
image: user.image,
},
}),
/**
* Part of the Keycloak fix/workaround, see code bellow for method `signOut`.
*
* @see https://stackoverflow.com/a/75526977
*/
jwt({ token, account }) {
if (account) {
token.id_token = account?.id_token;
token.provider = account?.provider;
}
return token;
},
},
adapter: adapter,
providers: [
KeycloakProvider({
clientId: env.KEYCLOAK_CLIENT_ID,
clientSecret: env.KEYCLOAK_CLIENT_SECRET,
issuer: env.KEYCLOAK_ISSUER,
// authorizationUrl: env.KEYCLOAK_ISSUER + "/protocol/openid-connect/auth",
// accessTokenUrl: env.KEYCLOAK_ISSUER + "/protocol/openid-connect/token",
// profileUrl: env.KEYCLOAK_ISSUER + "/protocol/openid-connect/userinfo",
// wellKnown: env.KEYCLOAK_ISSUER + "/.well-known/openid-configuration",
}),
/**
* ...add more providers here.
*
* Most other providers require a bit more work than the Discord provider. For example, the
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
*
* @see https://next-auth.js.org/providers/github
*/
],
events: {
/**
* Fix for Keycloak not destroying the session token on logout,
* we must send an extra request to delete the session.
*
* @see https://stackoverflow.com/a/75526977
*/
async signOut({ token }: { token: JWT }) {
if (token.provider === "keycloak") {
const logOutURL = new URL(
`${env.KEYCLOAK_ISSUER}/protocol/openid-connect/logout`
);
logOutURL.searchParams.set("id_token_hint", token.id_token ?? "");
await fetch(logOutURL);
}
},
},
};
/**
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
*
* @see https://next-auth.js.org/configuration/nextjs
*/
export const getServerAuthSession = (ctx: {
req: GetServerSidePropsContext["req"];
res: GetServerSidePropsContext["res"];
}) => {
return getServerSession(ctx.req, ctx.res, authOptions);
};

View file

@ -1,15 +0,0 @@
import { PrismaClient } from "@prisma/client";
import { env } from "~/env.mjs";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log:
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

View file

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -1,11 +0,0 @@
import { type Config } from "tailwindcss";
export default {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
],
} satisfies Config;

View file

@ -1,33 +0,0 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
}
},
"include": [
".eslintrc.cjs",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.cjs",
"**/*.mjs"
],
"exclude": ["node_modules"]
}