Compare commits

..

4 Commits

Author SHA1 Message Date
andrzej d667b45701 build out to docker tarball
Gitea/subman-nextjs/pipeline/head There was a failure building this commit Details
2024-10-01 22:48:58 +02:00
andrzej 136ee9ef6c fix dockerfile
Gitea/subman-nextjs/pipeline/head There was a failure building this commit Details
2024-10-01 16:57:06 +02:00
andrzej 6b3d43f040 transition to pnpm
Gitea/subman-nextjs/pipeline/head There was a failure building this commit Details
2024-10-01 15:04:42 +02:00
andrzej 9a3cd629fd begin docker setup
Gitea/subman-nextjs/pipeline/head This commit is unstable Details
2024-10-01 14:56:25 +02:00
28 changed files with 5304 additions and 392 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
.log
node_modules
.git
.next

9
.gitignore vendored
View File

@ -37,12 +37,3 @@ next-env.d.ts
#secret
.env
#build
/pack
subman.tar.gz
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

74
Dockerfile Normal file
View File

@ -0,0 +1,74 @@
FROM node:20-alpine AS base
### Dependencies ###
FROM base AS deps
RUN apk add --no-cache libc6-compat git
# Setup pnpm environment
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@latest --activate
## WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prefer-frozen-lockfile
# Builder
FROM base AS builder
RUN corepack enable
RUN corepack prepare pnpm@latest --activate
COPY prisma prisma
WORKDIR /app
COPY --from=deps /node_modules ./node_modules
COPY . .
RUN pnpm prisma db push
RUN pnpm build
### Production image runner ###
FROM base AS runner
# Set NODE_ENV to production
ENV NODE_ENV production
# Disable Next.js telemetry
# Learn more here: https://nextjs.org/telemetry
ENV NEXT_TELEMETRY_DISABLED 1
# Set correct permissions for nextjs user and don't run as root
RUN addgroup nodejs
RUN adduser -SDH nextjs
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:node:js /app/prisma ./prisma
USER nextjs
# Exposed port (for orchestrators and dynamic reverse proxies)
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "wget", "-q0", "http://localhost:3000/health" ]
# Run the nextjs app
CMD ["node", "server.js"]

29
Jenkinsfile vendored
View File

@ -7,36 +7,15 @@ agent any
stages{
stage('build'){
steps{
sh 'echo "JWT_SECRET=${JWT_SECRET}" | cat >> .env'
sh 'echo "DATABASE_URL=${DATABASE_URL}" | cat >> .env'
sh 'npm install'
sh 'npm run build'
}
}
stage('test'){
steps{
sh 'npx playwright install'
sh 'npx playwright test'
sh 'rm -r pack'
sh 'pnpm install'
sh 'docker build -t subman .'
sh 'docker buildx build --output type=tar . | gzip > subman.tar.gz'
}
}
stage('deploy'){
steps{
sshPublisher(publishers: [sshPublisherDesc(configName: 'Demos', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'ssh-uploads/subman/upgrade.sh', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'ssh-uploads/subman/', remoteDirectorySDF: false, removePrefix: '', sourceFiles: 'subman.tar.gz')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
sshPublisher(publishers: [sshPublisherDesc(configName: 'Demos', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'ssh-uploads/subman', remoteDirectorySDF: false, removePrefix: '', sourceFiles: 'subman.tar.gz')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}
}
post {
// Clean after build
always {
cleanWs(cleanWhenNotBuilt: true,
cleanWhenFailure: false,
deleteDirs: true,
disableDeferredWipeout: true,
// notFailBuild: true,
cleanWhenSuccess:false,
patterns: [[pattern: '**/*', type: 'INCLUDE'],
[pattern: '.propsfile', type: 'EXCLUDE']])
}
}
}

8
ecosystem.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
apps: [
{
name: "subman",
script: "npm run next start",
},
],
};

View File

@ -1,11 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
// basePath: "/subman",
webpack: (config) => {
config.externals = [...config.externals, "bcrypt"];
return config;
},
output: "standalone",
};
export default nextConfig;

83
package-lock.json generated
View File

@ -10,7 +10,6 @@
"dependencies": {
"@hookform/resolvers": "^3.6.0",
"@mapbox/node-pre-gyp": "^1.0.11",
"@next/env": "^14.2.14",
"@prisma/client": "^5.15.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-context-menu": "^2.2.1",
@ -33,7 +32,7 @@
"lucide-react": "^0.394.0",
"next": "^14.2.13",
"next-themes": "^0.3.0",
"playwright": "^1.47.2",
"prisma": "^5.15.0",
"react": "^18",
"react-day-picker": "^8.10.1",
"react-dom": "^18",
@ -45,14 +44,12 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@playwright/test": "^1.47.2",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.19",
"eslint-config-next": "14.2.3",
"postcss": "^8.4.38",
"prisma": "^5.15.0",
"tailwindcss": "^3.4.4",
"typescript": "^5"
}
@ -230,9 +227,10 @@
}
},
"node_modules/@next/env": {
"version": "14.2.14",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.14.tgz",
"integrity": "sha512-/0hWQfiaD5//LvGNgc8PjvyqV50vGK0cADYzaoOOGN8fxzBn3iAiaq3S0tCRnFBldq0LVveLcxCTi41ZoYgAgg=="
"version": "14.2.13",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.13.tgz",
"integrity": "sha512-s3lh6K8cbW1h5Nga7NNeXrbe0+2jIIYK9YaA9T7IufDWnZpozdFUp6Hf0d5rNWUKu4fEuSX2rCKlGjCrtylfDw==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.13",
@ -410,21 +408,6 @@
"node": ">= 8"
}
},
"node_modules/@playwright/test": {
"version": "1.47.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
"integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
"devOptional": true,
"dependencies": {
"playwright": "1.47.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@prisma/client": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.16.0.tgz",
@ -446,7 +429,6 @@
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.16.0.tgz",
"integrity": "sha512-OGvi/GvLX3XwTWQ+k/57kLyHGidQ8rC8zB+Zq9nEE7gegjazyzgLYN9qzfdcCfyI8ilc6IMxOyX4sspwkv98hg==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/debug": "5.16.0",
@ -458,20 +440,17 @@
"node_modules/@prisma/engines-version": {
"version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303.tgz",
"integrity": "sha512-HkT2WbfmFZ9WUPyuJHhkiADxazHg8Y4gByrTSVeb3OikP6tjQ7txtSUGu9OBOBH0C13dPKN2qqH12xKtHu/Hiw==",
"devOptional": true
"integrity": "sha512-HkT2WbfmFZ9WUPyuJHhkiADxazHg8Y4gByrTSVeb3OikP6tjQ7txtSUGu9OBOBH0C13dPKN2qqH12xKtHu/Hiw=="
},
"node_modules/@prisma/engines/node_modules/@prisma/debug": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.16.0.tgz",
"integrity": "sha512-pfdOGxMShqZKkNNskYB0yXICsqL6rOkQUKNktouUZ9Y9ASd5736+ae2fpzif7onwJiIyEpu/yvOO3rFUbliKTA==",
"devOptional": true
"integrity": "sha512-pfdOGxMShqZKkNNskYB0yXICsqL6rOkQUKNktouUZ9Y9ASd5736+ae2fpzif7onwJiIyEpu/yvOO3rFUbliKTA=="
},
"node_modules/@prisma/engines/node_modules/@prisma/fetch-engine": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.16.0.tgz",
"integrity": "sha512-8C8y6J9eWRl+R/aO3vQ2HlmM9IbjAmrZaaEAdC0OJfG3CHvbTOcL7VRY6CEUKo8RwZ8bdATOePaSMS634fHWgw==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.16.0",
"@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303",
@ -482,7 +461,6 @@
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.16.0.tgz",
"integrity": "sha512-ynp2jAYfYdd7OObX+uWaFRpvhPVmpF0nsRMhbrWdVVUj39q3Zr8dGz5WDj2g+BTUE++u1T1Am3RyM3PBQdDZXA==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.16.0"
}
@ -6647,11 +6625,6 @@
"react-dom": "^16.8 || ^17 || ^18"
}
},
"node_modules/next/node_modules/@next/env": {
"version": "14.2.13",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.13.tgz",
"integrity": "sha512-s3lh6K8cbW1h5Nga7NNeXrbe0+2jIIYK9YaA9T7IufDWnZpozdFUp6Hf0d5rNWUKu4fEuSX2rCKlGjCrtylfDw=="
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -7507,47 +7480,6 @@
"node": ">= 6"
}
},
"node_modules/playwright": {
"version": "1.47.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
"integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
"dependencies": {
"playwright-core": "1.47.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.47.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
"integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@ -7723,7 +7655,6 @@
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.16.0.tgz",
"integrity": "sha512-T1ZWJT/vgzp3rtRmd1iCSnPPsgOItXnnny+/cfpHraowiBEvUMD2pEI6yEOL6CP2EelTmq4wKDbXbYucy4Fd+A==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "5.16.0"

View File

@ -4,8 +4,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev",
"build": "next build && ./package.sh",
"build": "next build",
"start": "next start",
"lint": "next lint",
"tailwind": "npx tailwindcss -i ./src/app/globals.css -o ./src/app/tailwind.css --watch"
@ -13,7 +14,6 @@
"dependencies": {
"@hookform/resolvers": "^3.6.0",
"@mapbox/node-pre-gyp": "^1.0.11",
"@next/env": "^14.2.14",
"@prisma/client": "^5.15.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-context-menu": "^2.2.1",
@ -36,11 +36,11 @@
"lucide-react": "^0.394.0",
"next": "^14.2.13",
"next-themes": "^0.3.0",
"playwright": "^1.47.2",
"react": "^18",
"react-day-picker": "^8.10.1",
"react-dom": "^18",
"react-hook-form": "^7.51.5",
"prisma": "^5.15.0",
"recharts": "^2.12.7",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
@ -48,19 +48,18 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@playwright/test": "^1.47.2",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.19",
"eslint-config-next": "14.2.3",
"postcss": "^8.4.38",
"prisma": "^5.15.0",
"tailwindcss": "^3.4.4",
"typescript": "^5"
},
"overrides": {
"@radix-ui/react-dismissable-layer": "^1.0.5",
"@radix-ui/react-focus-scope": "^1.0.4"
}
},
"packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b"
}

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
mkdir pack
cp -r public pack/
cp -r prisma pack/
cp -rT .next/standalone pack/
cp -r .next/static pack/.next/
cd pack
npx prisma db push
cd ..
tar -cf subman.tar.gz pack

View File

@ -1,79 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'node pack/server.js',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});

5083
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

7
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,7 @@
packages:
# include packages in subfolders (e.g. apps/ and packages/)
- "apps/**"
- 'packages/**'
# if required, exclude some directories
- '!**/test/**'

Binary file not shown.

View File

@ -3,11 +3,7 @@ import prisma from 'app/lib/db';
import { jwtVerify, JWTPayload, decodeJwt, SignJWT } from 'jose';
import { cookies } from 'next/headers';
import { loginSchema, LoginSchema } from 'app/login/schema';
import { NextResponse } from 'next/server';
export async function getJWTSecretKey<Uint8Array>() {
const secret = process.env.JWT_SECRET

View File

@ -42,12 +42,12 @@ export default function RootLayout({
<footer className="my-auto md:mt-auto flex justify-center"><ModeToggle /><LogoutButton />
</footer>
</div>
<div className="flex justify-center w-screen">
<div className="flex justify-center w-full">
{children}
</div>
</div>
</div>
<Toaster test-id="toast" />
<Toaster />
</ThemeProvider>
</body>
</html >

View File

@ -36,7 +36,8 @@ export default function LoginForm() {
toast({ title: "login successful!" })
setSubmitted(true)
await revalidate(redirect)
window.location.href = redirect
//BUG:the first time user logs in, page refreshes instead of redirecting
router.push(redirect)
} else {
toast({ title: "login failed!" })
}
@ -44,15 +45,10 @@ export default function LoginForm() {
return (
<main className="flex flex-col items-center justify-around h-60 w-26">
{submitted ? <div className="flex flex-col items-center justify-around h-30 w-26">
<h1>Logging in...</h1>
<Button asChild>
<Link href={redirect}>Continue</Link>
</Button>
</div> :
<>
{submitted ? <p>Logging in...</p> :
<Form {...form}>
<form onSubmit={onSubmit} className="flex flex-col items-center space-y-6">
<form onSubmit={onSubmit} className="mt-20 flex flex-col items-center space-y-6">
<FormField
control={form.control}
name="email"
@ -83,7 +79,7 @@ export default function LoginForm() {
</form>
</Form>
}
</main>
</>
)
}

View File

@ -1,28 +1,13 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import styles from "./page.module.css";
export default function Home() {
return (
<main className="flex flex-col gap-4 items-center justify-around h-60 w-26 m-6">
< div >
<h1 className="text-3xl font-black">
Welcome to Subman!
</h1>
</div >
<div className="flex flex-col gap-3">
<p>
This app is for demonstration purposes only. Data is reset periodically.
</p>
<p>
<b>USERNAME:</b> demo@demo.demo
</p>
<p>
<b>PASSWORD:</b> password
</p>
</div>
<Button className="mt-6">
<Link href="/login">Log in</Link>
</Button>
</main >
<main className={styles.main}>
Hello
<h1 className="text-3xl font-black underline">
Hello world!
</h1>
</main>
);
}

View File

@ -1,9 +1,12 @@
"use client"
import { DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Dialog, DialogHeader, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { ComponentProps } from "react";
import { Genre, Pub } from "@prisma/client";
import { createPub } from "app/lib/create";
import PubForm from "app/ui/forms/pub";
import { Plus } from "lucide-react";
import { useState } from "react";
import { PubWithGenres } from "./page";
export default function EditPubDialog({ genres, closeDialog, defaults, dbAction }: ComponentProps<"div"> & { genres: Genre[], closeDialog: () => void, defaults: PubWithGenres, dbAction: (data: Pub & { genres: number[] }) => Promise<{ success: string }> }) {
@ -14,7 +17,7 @@ export default function EditPubDialog({ genres, closeDialog, defaults, dbAction
<>
<DialogHeader>
<DialogTitle>Edit publication</DialogTitle>
<DialogDescription>Modify an entry for an existing publication. Remember - you can edit fields inline by double clicking on them!</DialogDescription>
<DialogDescription>Modify an entry for an existing publication.</DialogDescription>
</DialogHeader>
<PubForm dbAction={dbAction} genres={genres} closeDialog={closeDialog} defaults={defaults} />
<DialogFooter>

View File

@ -14,7 +14,7 @@ export default function EditStoryDialog({ genres, closeDialog, defaults, dbActio
<>
<DialogHeader>
<DialogTitle>Edit story</DialogTitle>
<DialogDescription>Modify an entry for an existing story. Remember - you can edit fields inline by double-clicking on them!</DialogDescription>
<DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription>
</DialogHeader>
<StoryForm dbAction={dbAction} genres={genres} className="" closeDialog={closeDialog} defaults={defaults} />
<DialogFooter>

View File

@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
import { SubComplete } from "./page"
import { selectCol } from "app/ui/tables/selectColumn"
import TitleContainer from "app/ui/titleContainer"
import { CalendarArrowUp } from "lucide"

View File

@ -8,12 +8,10 @@ import { Pub, Response, Story } from "@prisma/client";
import SubmissionForm from "app/ui/forms/sub";
import { Plus } from "lucide-react";
import { useState } from "react";
import { StoryWithGenres } from "app/story/page";
import { PubWithGenres } from "app/publication/page";
export default function CreateSubmissionDialog({ stories, pubs, responses }: ComponentProps<"div"> & { stories: StoryWithGenres[], pubs: PubWithGenres[], responses: Response[] }) {
export default function CreateSubmissionDialog({ stories, pubs, responses }: ComponentProps<"div"> & { stories: Story[], pubs: Pub[], responses: Response[] }) {
const [isOpen, setIsOpen] = useState(false)
function closeDialog() {

View File

@ -0,0 +1,22 @@
"use server"
import { getPubs, getResponses, getStories } from "app/lib/get";
import SubmissionForm from "app/ui/forms/sub";
import prisma from "app/lib/db";
import { CreateContainer, CreateContainerContent, CreateContainerHeader } from "app/ui/createContainer";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export default async function Page() {
const stories = await getStories()
const pubs = await getPubs()
const responses = await getResponses()
return (
<CreateContainer>
<CreateContainerHeader>New submission</CreateContainerHeader>
<CreateContainerContent>
<SubmissionForm stories={stories} pubs={pubs} responses={responses} />
</CreateContainerContent>
</CreateContainer>
)
}

View File

@ -1,10 +1,8 @@
import { getGenres, getPubs, getPubsWithGenres, getResponses, getStories, getStoriesWithGenres, getSubsComplete } from "app/lib/get"
import { getGenres, getPubs, getResponses, getStories, getSubsComplete } from "app/lib/get"
import { DataTable } from "app/ui/tables/data-table"
import { columns } from "./columns"
import { Genre, Pub, Response, Story, Sub } from "@prisma/client"
import { Pub, Response, Story, Sub } from "@prisma/client"
import CreateSubmissionDialog from "./create"
import { PubWithGenres } from "app/publication/page"
import { StoryWithGenres } from "app/story/page"
export type SubComplete = Sub & {
pub: Pub,
@ -14,10 +12,10 @@ export type SubComplete = Sub & {
export default async function Page() {
const subs: Array<SubComplete> = await getSubsComplete()
const stories: StoryWithGenres[] = await getStoriesWithGenres()
const pubs: PubWithGenres[] = await getPubsWithGenres()
const responses: Response[] = await getResponses()
const genres: Genre[] = await getGenres()
const stories = await getStories()
const pubs = await getPubs()
const responses = await getResponses()
const genres = await getGenres()
return (
<div className="container px-1 md:px-4 mx-auto">

View File

@ -706,10 +706,6 @@ body {
z-index: 100;
}
.m-6 {
margin: 1.5rem;
}
.m-auto {
margin: auto;
}
@ -719,11 +715,6 @@ body {
margin-right: -0.25rem;
}
.mx-0 {
margin-left: 0px;
margin-right: 0px;
}
.mx-1 {
margin-left: 0.25rem;
margin-right: 0.25rem;
@ -774,6 +765,10 @@ body {
margin-top: 0.5rem;
}
.mt-20 {
margin-top: 5rem;
}
.mt-3 {
margin-top: 0.75rem;
}
@ -855,10 +850,6 @@ body {
height: 1.25rem;
}
.h-60 {
height: 15rem;
}
.h-7 {
height: 1.75rem;
}
@ -1142,10 +1133,6 @@ body {
gap: 0.5rem;
}
.gap-3 {
gap: 0.75rem;
}
.gap-4 {
gap: 1rem;
}
@ -1625,6 +1612,10 @@ body {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}
.underline-offset-4 {
text-underline-offset: 4px;
}

View File

@ -36,14 +36,12 @@ import { createSub } from "app/lib/create"
import { subSchema } from "./schemas"
import { useRouter } from "next/navigation"
import { Ban } from "lucide-react"
import { Pub, Response, Story, Sub } from "@prisma/client"
import { StoryWithGenres } from "app/story/page"
import { PubWithGenres } from "app/publication/page"
import { Story } from "@prisma/client"
export type SubForm = z.infer<typeof subSchema>
export default function SubmissionForm({ stories, pubs, responses, defaults, closeDialog }: { stories: StoryWithGenres[], pubs: PubWithGenres[], responses: Response[], defaults?: Sub, closeDialog?: () => void }) {
export default function SubmissionForm({ stories, pubs, responses, defaults, closeDialog }: { stories: any, pubs: any, responses: any, defaults?: any, closeDialog?: () => void }) {
const form = useForm<z.infer<typeof subSchema>>({
resolver: zodResolver(subSchema),
defaultValues: {
@ -53,41 +51,19 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
})
const [isSubCalendarOpen, setIsSubCalendarOpen] = useState(false);
const [isRespCalendarOpen, setIsRespCalendarOpen] = useState(false);
const [relevantPubIds, setRelevantPubIds] = useState(pubs.map(e => e.id));
function updateRelevantPubs(storyId: number) {
console.log("storyId: " + storyId)
console.log("stories: ", stories)
const story = stories.find(e => e.id == storyId)
console.log("story: ", story)
const storyGenreIds = story?.genres.map(e => e.id) ?? []
const relevantPubIds = pubs.filter(e => {
const pubGenreIds = e.genres.map(e => e.id)
for (let i = 0; i < storyGenreIds.length; i++) {
const storyGenreId = storyGenreIds[i];
if (pubGenreIds.includes(storyGenreId)) return true
}
}).map(e => e.id)
console.log("relevant pubs: ", relevantPubIds)
setRelevantPubIds(relevantPubIds)
}
const storiesSelectItems = stories.map((e: Story) => (
<SelectItem value={e.id?.toString()} key={e.title}>
{e.title}
</SelectItem>
))
const pubsSelectItems = pubs.map(e => {
const isDisabled = !relevantPubIds.includes(e.id)
return (
<SelectItem disabled={isDisabled} value={e.id.toString()} key={e.title}>
{e.title}
</SelectItem>
)
})
const pubsSelectItems = pubs.map(e => (
<SelectItem value={e.id} key={e.title}>
{e.title}
</SelectItem>
))
const reponsesSelectItems = responses.map(e => (
<SelectItem value={e.id.toString()} key={e.response}>
<SelectItem value={e.id} key={e.title}>
{e.response}
</SelectItem>
))
@ -151,8 +127,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm md:text-base">Publication</FormLabel>
<Select onOpenChange={() => updateRelevantPubs(form.getValues().storyId
)} onValueChange={field.onChange} defaultValue={field.value?.toString()}>
<Select onValueChange={field.onChange} defaultValue={field.value?.toString()}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select something">
@ -164,7 +139,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
{pubsSelectItems}
</SelectContent>
</Select>
<FormDescription className="text-xs md:text-base">The market you sent it to. Bad genre fits are greyed out.</FormDescription>
<FormDescription className="text-xs md:text-base">The market you sent it to</FormDescription>
<FormMessage />
</FormItem>
)}

View File

@ -2,6 +2,8 @@
import { Button } from "@/components/ui/button"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import {
@ -13,7 +15,7 @@ import {
DropdownMenuRadioGroup
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { ComponentProps, useState } from "react"
import { Component, ComponentProps, use, useState } from "react"
import {
ColumnDef,
flexRender,
@ -36,7 +38,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
import { CircleHelp, EyeIcon, Filter, Trash2 } from "lucide-react"
import { EyeIcon, Trash2 } from "lucide-react"
import { usePathname, useSearchParams } from "next/navigation"
import FormContextMenu from "./contextMenu"
import { deleteRecord, deleteRecords } from "app/lib/del"
@ -127,14 +129,16 @@ export function DataTable<TData, TValue>({
const router = useRouter()
const [filterBy, setFilterBy] = useState(table.getAllColumns().filter(e => e.getCanFilter())[0])
const [filterBy, setFilterBy] = useState(table.getAllColumns()[0])
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
return (<>
<div className="flex gap-2 justify-between items-center py-1 md:py-4">
<div className="flex gap-1">
<div className="flex justify-between items-center py-1 md:py-4">
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="mx-0"> <p className="hidden md:block">Filter by</p><Filter className="block md:hidden" /> </Button>
<Button variant="outline" className="hidden sm:block ml-auto">
Filter by
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/*@ts-ignore*/}
@ -304,45 +308,36 @@ export function DataTable<TData, TValue>({
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => {
const classes = () => {
const classes = []
if (row.getValue('response') === "Pending") classes.push("bg-accent")
if (row.getValue('response') === "Acceptance") classes.push("bg-primary")
return classes.join(" ")
}
return (
<ContextMenu key={row.id + "contextMenu"}>
<ContextMenuTrigger asChild>
<TableRow
key={row.id}
className={classes()}
data-state={row.getIsSelected() && "selected"}
tabIndex={0}
onDoubleClick={() => {
if (tableName === "sub") {
openEditDialog(row)
}
table.getRowModel().rows.map((row) => (
<ContextMenu key={row.id + "contextMenu"}>
<ContextMenuTrigger asChild>
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
tabIndex={0}
onDoubleClick={() => {
if (tableName === "sub") {
openEditDialog(row)
}
}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
<FormContextMenu
key={"formContextMenu" + row.id}
row={row}
table={table}
openEditDialog={openEditDialog}
openDeleteDialog={openDeleteDialog}
/>
</TableRow>
</ContextMenuTrigger>
</ContextMenu>
)
})
}
}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
<FormContextMenu
key={"formContextMenu" + row.id}
row={row}
table={table}
openEditDialog={openEditDialog}
openDeleteDialog={openDeleteDialog}
/>
</TableRow>
</ContextMenuTrigger>
</ContextMenu>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">

View File

@ -16,7 +16,7 @@ export const selectCol = {
)
},
enableColumnFilter: false,
cell: (props: CellContext<any, any>) => {
return (
<div className="flex items-start justify-left">

View File

@ -1,31 +0,0 @@
import { test, expect } from '@playwright/test'
test('should redirect to login page if not logged in', async ({ page }) => {
await page.goto('/')
await page.click('text=Stories')
await expect(page).toHaveURL('/login?from=%2Fstory')
await page.click('text=Publications')
await expect(page).toHaveURL('/login?from=%2Fpublication')
await page.click('text=Submissions')
await expect(page).toHaveURL('/login?from=%2Fsubmission')
})
test('positive login', async ({ page }) => {
await page.goto('/login')
await page.getByRole('textbox', { name: 'email' }).fill('demo@demo.demo')
await page.getByRole('textbox', { name: 'password' }).fill('password')
await page.getByRole('button', { name: 'submit' }).click()
await page.waitForURL('**/submission', { timeout: 5000 })
await expect(page).toHaveURL('/submission');
})
test('negative login', async ({ page }) => {
await page.goto('/login')
await page.getByRole('textbox', { name: 'email' }).fill('demo@demo.negative')
await page.getByRole('textbox', { name: 'password' }).fill('negative')
await page.getByRole('button', { name: 'submit' }).click()
await expect(page.getByText("login failed!")).toBeTruthy()
})