Initial commit: RTS Application
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Docker / root env
|
||||
.env
|
||||
.env.local
|
||||
.env*.local
|
||||
|
||||
# Next.js
|
||||
node/node_modules/
|
||||
node/.next/
|
||||
node/next-env.d.ts
|
||||
|
||||
# Next.js app-level env (in case anything writes one here too)
|
||||
node/.env
|
||||
node/.env.local
|
||||
node/.env*.local
|
||||
|
||||
# leftover/archived code
|
||||
trash/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.log
|
||||
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM node:current-alpine3.22
|
||||
RUN apk update && apk add su-exec
|
||||
WORKDIR /node
|
||||
COPY ./node/package.json ./node/package-lock.json ./
|
||||
RUN npm install && chown -R node:node .
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["npm", "run", "dev"]
|
||||
23
docker-compose.yml
Executable file
@ -0,0 +1,23 @@
|
||||
services:
|
||||
app:
|
||||
container_name: rts-app
|
||||
build: .
|
||||
image: rts-app:latest
|
||||
#user: "node"
|
||||
environment:
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
networks:
|
||||
- proxy
|
||||
volumes:
|
||||
- ./node:/node
|
||||
- nextjs_cache:/node/.next
|
||||
- node_modules:/node/node_modules
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
nextjs_cache:
|
||||
node_modules:
|
||||
4
entrypoint.sh
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
# Fix volume ownership then drop to node user
|
||||
chown -R node:node /node/.next /node/node_modules
|
||||
exec su-exec node "$@"
|
||||
41
node/.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
node/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
16
node/eslint.config.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
7
node/jsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
7
node/next.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
/* config options here */
|
||||
allowedDevOrigins: ['rts.rynohome.com'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6813
node/package-lock.json
generated
Normal file
25
node/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "node",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS='--max-old-space-size=1024' next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"jose": "^6.2.3",
|
||||
"next": "16.2.4",
|
||||
"pg": "^8.20.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.4",
|
||||
"tailwindcss": "^4"
|
||||
}
|
||||
}
|
||||
7
node/postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
node/public/file.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
node/public/globe.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
node/public/next.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
node/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
node/public/window.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
25
node/src/app/(admin)/admin/dashboard/page.js
Normal file
@ -0,0 +1,25 @@
|
||||
import style from '@/styles/pages/Dashboard.module.css';
|
||||
|
||||
export default function DashboardPage()
|
||||
{
|
||||
return (
|
||||
<div className={style["container"]}>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>This is the dashboard page. You can add your dashboard content here.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>This is the dashboard page. You can add your dashboard content here.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>This is the dashboard page. You can add your dashboard content here.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>This is the dashboard page. You can add your dashboard content here.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
node/src/app/(admin)/admin/inventory/page.js
Normal file
@ -0,0 +1,13 @@
|
||||
import style from '@/styles/pages/Dashboard.module.css';
|
||||
|
||||
export default function InventoryPage()
|
||||
{
|
||||
return (
|
||||
<div className={style["container"]}>
|
||||
<div>
|
||||
<h1>Inventory</h1>
|
||||
<p>This is the inventory page. You can add your inventory content here.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
node/src/app/(admin)/layout.js
Normal file
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
|
||||
import "@/styles/global.css"
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import NextImage from 'next/image';
|
||||
|
||||
import RtsLogo from'@/res/rts_logo.svg';
|
||||
|
||||
import styles from '@/styles/Layout.module.css';
|
||||
|
||||
import Navbar from '@/components/navbar/Navbar';
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
|
||||
const router = useRouter();
|
||||
async function handleLogout() {
|
||||
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["header"]}>
|
||||
<div className={styles["logo-container"]}>
|
||||
<NextImage src={RtsLogo} alt="RTS Logo" fill style={{ objectFit: 'contain' }} />
|
||||
</div>
|
||||
<div className={styles["navbar-container"]}>
|
||||
<Navbar links={[{ label: 'Dashboard', type: 'link', href: '/admin/dashboard' },
|
||||
{ label: 'Inventory', type: 'link', href: '/admin/inventory' },
|
||||
{ label: 'Work Orders', type: 'link', href: '/admin/work-orders' },
|
||||
{ label: 'Procurement', type: 'link', href: '/admin/procurement' },
|
||||
{ label: 'ECO', type: 'link', href: '/eco' },
|
||||
]}/>
|
||||
</div>
|
||||
<div className={styles["userbar-container"]}>
|
||||
<Navbar links={[{ label: 'Home', type: 'link', href: '/' },
|
||||
{ label: 'Account', type: 'link', href: '/account' },
|
||||
{ label: 'Logout', type: 'action', onClick: handleLogout },
|
||||
]}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["content-container"]}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
node/src/app/(auth)/account/page.js
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
export default function AccountPage()
|
||||
{
|
||||
return (
|
||||
<div>
|
||||
<h1>Account</h1>
|
||||
<p>Welcome to the account page! This page is protected and requires authentication.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
66
node/src/app/(auth)/layout.js
Normal file
@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import "@/styles/global.css"
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import NextImage from 'next/image';
|
||||
|
||||
import RtsLogo from'@/res/rts_logo.svg';
|
||||
|
||||
import styles from '@/styles/Layout.module.css';
|
||||
|
||||
|
||||
import Navbar from '@/components/navbar/Navbar';
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
|
||||
const router = useRouter();
|
||||
const [userData, setUserData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUserData() {
|
||||
const result = await fetch('/api/auth/me', { method: 'GET' });
|
||||
const data = await result.json();
|
||||
setUserData(data);
|
||||
}
|
||||
|
||||
fetchUserData();
|
||||
}, []);
|
||||
|
||||
async function handleLogout() {
|
||||
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
|
||||
const userbarLinks = userData?.admin === true ? [ { label: 'Dashboard', type: 'link', href: '/admin/dashboard' },
|
||||
{ label: 'Account', type: 'link', href: '/account' },
|
||||
{ label: 'Logout', type: 'action', onClick: handleLogout },
|
||||
] :
|
||||
[ { label: 'Account', type: 'link', href: '/account' },
|
||||
{ label: 'Logout', type: 'action', onClick: handleLogout },
|
||||
]
|
||||
|
||||
console.log('userData:', userData);
|
||||
console.log('userbarLinks:', userbarLinks);
|
||||
return (
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["header"]}>
|
||||
<div className={styles["logo-container"]}>
|
||||
<NextImage src={RtsLogo} alt="RTS Logo" fill style={{ objectFit: 'contain' }} />
|
||||
</div>
|
||||
<div className={styles["navbar-container"]}>
|
||||
<Navbar links={[{ label: 'Home', type: 'link', href: '/' },
|
||||
{ label: 'Shop', type: 'link', href: '/shop' },
|
||||
]}/>
|
||||
</div>
|
||||
<div className={styles["userbar-container"]}>
|
||||
<Navbar links={userbarLinks}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["content-container"]}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
node/src/app/(auth)/page.js
Normal file
@ -0,0 +1,9 @@
|
||||
export default function HomePage()
|
||||
{
|
||||
return (
|
||||
<div>
|
||||
<h1>Home</h1>
|
||||
<p>Welcome to the home page! This page is protected and requires authentication.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
node/src/app/(auth)/shop2/page.js
Normal file
@ -0,0 +1,9 @@
|
||||
export default function ShopPage()
|
||||
{
|
||||
return (
|
||||
<div>
|
||||
<h1>Shop</h1>
|
||||
<p>Welcome to the shop page! This page is protected and requires authentication.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
node/src/app/(public)/layout.js
Normal file
@ -0,0 +1,9 @@
|
||||
import "@/styles/global.css"
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
node/src/app/(public)/login/page.js
Normal file
@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
import '@/styles/global.css'
|
||||
import '@/styles/card.css'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [remember, setRemember] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
|
||||
const res = await fetch('/api/public/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, remember })
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
router.push('/')
|
||||
} else {
|
||||
const data = await res.json()
|
||||
setError(data.message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="login-page" className="center-page">
|
||||
<div className="card-container">
|
||||
|
||||
<div className="card-header">
|
||||
<h1>Welcome Back!</h1>
|
||||
<p>Please enter your credentials to log in.</p>
|
||||
</div>
|
||||
|
||||
<form className="card-body" onSubmit={handleSubmit}>
|
||||
<div className="card-entry">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card-entry">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="card-error">*{error}</p>}
|
||||
|
||||
<div className="card-actions">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={remember}
|
||||
onChange={e => setRemember(e.target.checked)}
|
||||
/>
|
||||
Remember me
|
||||
</label>
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Logging in...' : 'Log In'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="card-footer">
|
||||
<p>Don't have an account? <a href="/signup">Sign Up</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
node/src/app/(public)/shop/page.js
Normal file
@ -0,0 +1,12 @@
|
||||
import {Navbar } from '@/components/navbar/Navbar';
|
||||
|
||||
export default function ShopPage()
|
||||
{
|
||||
return (
|
||||
<div>
|
||||
<Navbar />
|
||||
<h1>Shop</h1>
|
||||
<p>Welcome to the shop page! This page is protected and requires authentication.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
156
node/src/app/(public)/signup/page.js
Normal file
@ -0,0 +1,156 @@
|
||||
'use client'
|
||||
import '@/styles/global.css'
|
||||
import '@/styles/card.css'
|
||||
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
|
||||
export default function SignupPage() {
|
||||
const [fname, setFName] = useState('')
|
||||
const [lname, setLName] = useState('')
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [rpassword, setRPassword] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [error, setError] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
function validate(fname, lname, username, email, password) {
|
||||
if (fname.length < 2) return 'First name must be at least 2 characters'
|
||||
if (lname.length < 2) return 'Last name must be at least 2 characters'
|
||||
if (username.length < 3) return 'Username must be at least 3 characters'
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(username)) return 'Username can only contain letters, numbers, and underscores'
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Invalid email format'
|
||||
if (password.length < 8) return 'Password must be at least 8 characters'
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
|
||||
if(rpassword !== password) {
|
||||
setError('Passwords must match')
|
||||
return
|
||||
}
|
||||
|
||||
const validationError = validate(fname, lname, username, email, password)
|
||||
if (validationError) {
|
||||
setError(validationError)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
const res = await fetch('/api/public/signup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fname, lname, username, email, password })
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
router.push('/login')
|
||||
} else {
|
||||
const data = await res.json()
|
||||
setError(data.message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="signup-page" className="center-page">
|
||||
<div className="card-container">
|
||||
|
||||
<div className="card-header">
|
||||
<h1>Create an Account</h1>
|
||||
<p>Please enter your details to sign up.</p>
|
||||
</div>
|
||||
|
||||
<form className="card-body" onSubmit={handleSubmit}>
|
||||
<div className="card-entry">
|
||||
<label htmlFor="fname">First Name</label>
|
||||
<input
|
||||
id="fname"
|
||||
type="text"
|
||||
placeholder="First Name"
|
||||
value={fname}
|
||||
onChange={e => setFName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="card-entry">
|
||||
<label htmlFor="lname">Last Name</label>
|
||||
<input
|
||||
id="lname"
|
||||
type="text"
|
||||
placeholder="Last Name"
|
||||
value={lname}
|
||||
onChange={e => setLName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="card-entry">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="card-entry">
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="card-entry">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="card-entry">
|
||||
<label htmlFor="confirmPassword">Repeat Password</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Repeat Password"
|
||||
value={rpassword}
|
||||
onChange={e => setRPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="card-error">{error}</p>}
|
||||
|
||||
<div className="card-actions">
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Signing up...' : 'Sign Up'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="card-footer">
|
||||
<p>Already have an account? <a href="/login">Log In</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
14
node/src/app/api/auth/logout/route.js
Normal file
@ -0,0 +1,14 @@
|
||||
// app/api/auth/logout/route.js
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function POST() {
|
||||
const response = NextResponse.json({ message: 'Ok' }, { status: 200 })
|
||||
|
||||
response.cookies.set('session', '', {
|
||||
httpOnly: true,
|
||||
maxAge: 0, // immediately expires
|
||||
path: '/'
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
24
node/src/app/api/auth/me/route.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { jwtVerify } from 'jose';
|
||||
import { SECRET, ADMIN_ROLE_ID} from '@/lib/auth';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session')?.value;
|
||||
console.log('token:', token);
|
||||
|
||||
if (!token) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { payload } = await jwtVerify(token, SECRET);
|
||||
console.log('payload:', payload);
|
||||
|
||||
const isAdmin = (payload.roleId === ADMIN_ROLE_ID) ? true : false;
|
||||
|
||||
return NextResponse.json({ admin: isAdmin, username: payload.username });
|
||||
} catch (err) {
|
||||
//loveyou
|
||||
return NextResponse.json({ message: 'Invalid session' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
58
node/src/app/api/public/login/route.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { SignJWT } from 'jose'
|
||||
import { loginUser } from '@/lib/db/UserService'
|
||||
import { SECRET } from '@/lib/auth'
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const { username, password, remember } = await request.json()
|
||||
|
||||
if (!username || !password) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Username and password are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const user = await loginUser(username, password);
|
||||
|
||||
// Short JWT for normal login — 8 hours
|
||||
// Long JWT for remember me — 30 days
|
||||
const expiresIn = remember ? '30d' : '8h'
|
||||
const maxAge = remember ? 60 * 60 * 24 * 30 : 60 * 60 * 8
|
||||
|
||||
// create JWT token
|
||||
const token = await new SignJWT({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
roleId: user.role_id
|
||||
})
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(expiresIn)
|
||||
.sign(SECRET)
|
||||
|
||||
// Set cookie on response
|
||||
const response = NextResponse.json({ message: 'Ok' }, { status: 200 })
|
||||
response.cookies.set('session', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: maxAge,
|
||||
path: '/'
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
const errorMessages = {
|
||||
USER_NOT_FOUND: { message: 'User not found', status: 404 },
|
||||
INVALID_CREDENTIALS: { message: 'Invalid credentials', status: 401 }
|
||||
}
|
||||
|
||||
const known = errorMessages[error.message]
|
||||
if (!known) console.error('Login error:', error)
|
||||
|
||||
const { message, status } = known || { message: 'Internal server error', status: 500 }
|
||||
return NextResponse.json({ message }, { status })
|
||||
}
|
||||
}
|
||||
45
node/src/app/api/public/signup/route.js
Normal file
@ -0,0 +1,45 @@
|
||||
import {NextResponse} from 'next/server'
|
||||
|
||||
import { Query } from '@/lib/db/db'
|
||||
import { addUser } from '@/lib/db/UserService'
|
||||
|
||||
export async function POST(request) {
|
||||
|
||||
try {
|
||||
const { fname, lname, username, email, password } = await request.json()
|
||||
|
||||
if (!fname || !lname || !username || !password || !email) {
|
||||
return NextResponse.json(
|
||||
{ message: 'First name, last name, username, password, and email are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const user = await addUser(fname, lname, username, email, password)
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Failed to create user' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Ok',
|
||||
user: user
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessages = {
|
||||
USERNAME_EXISTS: { message: 'Username already exists', status: 409 },
|
||||
EMAIL_EXISTS: { message: 'Email already exists', status: 409 }
|
||||
}
|
||||
|
||||
const known = errorMessages[error.message]
|
||||
if (!known) console.error('Signup error:', error)
|
||||
|
||||
const { message, status } = known || { message: 'Internal server error', status: 500 }
|
||||
return NextResponse.json({ message }, { status })
|
||||
}
|
||||
}
|
||||
9
node/src/app/icon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg viewBox="0 0 126 144" role="img" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(63, 72)">
|
||||
<polygon points="0,-66 57,-33 57,33 0,66 -57,33 -57,-33" fill="#273b6b"/>
|
||||
<polygon points="0,-54 46,-27 46,27 0,54 -46,27 -46,-27" fill="#1e40af"/>
|
||||
<polygon points="0,-54 46,-27 46,27 0,54 -46,27 -46,-27" fill="none" stroke="#3370d1" stroke-width="1" opacity="0.5"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" y="1" style="fill:#ffffff;font-family:system-ui,sans-serif;font-size:32px;font-weight:500;">RTS</text>
|
||||
<rect x="-26" y="42" width="52" height="2" rx="1" fill="#dbeafe" opacity="0.35"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 650 B |
14
node/src/app/layout.js
Normal file
@ -0,0 +1,14 @@
|
||||
import "@/styles/global.css"
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Rynotech Services</title>
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
27
node/src/components/navbar/Navbar.js
Normal file
@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import styles from './Navbar.module.css';
|
||||
|
||||
export default function Navbar({ links = [] }) {
|
||||
return (
|
||||
<div className={styles["navbar"]}>
|
||||
<ul className={styles["links"]}>
|
||||
{links.map((link) => (
|
||||
<li className={styles["link-container"]} key={link.label}>
|
||||
{link.type === 'action' ? (
|
||||
<button onClick={link.onClick} className={styles["link"]}>
|
||||
{link.label}
|
||||
</button>
|
||||
) : (
|
||||
<Link href={link.href} className={styles["link"]}>
|
||||
{link.label}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
node/src/components/navbar/Navbar.module.css
Normal file
@ -0,0 +1,57 @@
|
||||
.navbar
|
||||
{
|
||||
--text-size: var(--text-size-medium);
|
||||
|
||||
display: flex;
|
||||
height: var(--navbar-height);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.links
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.link-container
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.link
|
||||
{
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
background-color: var(--primary-color);
|
||||
border: 2px solid var(--primary-color-dark);
|
||||
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
font-size: var(--text-size-medium);
|
||||
border-radius: 10px;
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
|
||||
.link:hover
|
||||
{
|
||||
background-color: var(--primary-color-highlight);
|
||||
}
|
||||
|
||||
.link:active
|
||||
{
|
||||
background-color: var(--primary-color-dark);
|
||||
}
|
||||
|
||||
.link.visited
|
||||
{
|
||||
color: rgb(183, 18, 18);
|
||||
}
|
||||
12
node/src/itc/app/itc/page.js
Normal file
@ -0,0 +1,12 @@
|
||||
import '@/styles/itc/machines/itc-machine.css'
|
||||
|
||||
import Itc55100 from '@/components/itc/machines/itc55100/Itc55100'
|
||||
|
||||
export default function ItcMachine()
|
||||
{
|
||||
return (
|
||||
<div id="itc-machine-container" >
|
||||
<Itc55100 />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
node/src/itc/components/itc/comps/ItcButton.js
Normal file
@ -0,0 +1,9 @@
|
||||
import '@/styles/itc/comps/itc-button.css'
|
||||
|
||||
export default function ItcButton( {style, name, onClick} ) {
|
||||
return (
|
||||
<div id="itc-button-container">
|
||||
<button className={style} onClick={onClick}>{name}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
node/src/itc/components/itc/comps/ItcCombo.js
Normal file
@ -0,0 +1,21 @@
|
||||
import styles from '@/styles/itc/comps/itc-combo.module.css'
|
||||
|
||||
export default function ItcCombo( { label, values, unit, onChange, disable} ) {
|
||||
return (
|
||||
<div id="itc-entry-container" className={styles['container']}>
|
||||
<div className={styles['label-container']}>
|
||||
<p>{label}:</p>
|
||||
</div>
|
||||
<div className={`${styles['entry-container']} ${disable ? styles['disabled'] : ''}`}>
|
||||
<select onChange={onChange}>
|
||||
{values.map((v) => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className={styles['unit-container']}>
|
||||
<p>{unit}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
node/src/itc/components/itc/comps/ItcEntry.js
Normal file
@ -0,0 +1,17 @@
|
||||
import styles from '@/styles/itc/comps/itc-entry.module.css'
|
||||
|
||||
export default function ItcEntry( { label, value = '', unit, disable} ) {
|
||||
return (
|
||||
<div id="itc-entry-container" className={styles['container']}>
|
||||
<div className={styles['label-container']}>
|
||||
<p>{label}:</p>
|
||||
</div>
|
||||
<div className={styles['entry-container']}>
|
||||
<input defaultValue={value}/>
|
||||
</div>
|
||||
<div className={styles['unit-container']}>
|
||||
<p>{unit}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
node/src/itc/components/itc/comps/ItcKeyboard.js
Normal file
40
node/src/itc/components/itc/machines/itc55100/Itc55100.js
Normal file
@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import NextImage from 'next/image';
|
||||
|
||||
import styles from '@/styles/itc/machines/itc55100/itc55100.module.css';
|
||||
|
||||
import LeftEdge from'@/res/itc/machines/itc55100/LeftEdgeCurve.svg';
|
||||
|
||||
|
||||
import Home from '@/components/itc/machines/itc55100/panels/Home';
|
||||
import TestSpec from '@/components/itc/machines/itc55100/panels/TestSpec'
|
||||
|
||||
const panels = {
|
||||
home: Home,
|
||||
testspec : TestSpec
|
||||
};
|
||||
|
||||
export default function Itc55100()
|
||||
{
|
||||
const nextConfig = {
|
||||
images: {
|
||||
dangerouslyAllowSVG: true,
|
||||
contentDispositionType: 'attachment',
|
||||
},
|
||||
};
|
||||
|
||||
const [page, setPage] = useState('home')
|
||||
|
||||
const Panel = panels[page];
|
||||
return (
|
||||
<div id="itc55100-container" className={styles.container}>
|
||||
<div className={styles['left-edge']}>
|
||||
<NextImage src={LeftEdge} alt="Left Edge" fill style={{ objectFit: 'fill' }} />
|
||||
</div>
|
||||
<div className={styles['panel'] }>
|
||||
{Panel ? <Panel navigate={setPage}/> : <div>Unknown panel</div> }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
node/src/itc/components/itc/machines/itc55100/panels/Home.js
Normal file
@ -0,0 +1,36 @@
|
||||
import styles from '@/styles/itc/machines/itc55100/panels/home.module.css'
|
||||
|
||||
import ItcButton from '@/components/itc/comps/ItcButton'
|
||||
|
||||
export default function HomePanel({ navigate })
|
||||
{
|
||||
return (
|
||||
<div id="itc55100-home-panel" className={styles['container']}>
|
||||
<div className={styles['info-bar']}>
|
||||
<p>GUI Revision: 30-May-2026</p>
|
||||
<p>PLD Revision: 49, 12W</p>
|
||||
<p>Firmware Revision: 30-May-2026</p>
|
||||
</div>
|
||||
<div className={styles['title-bar']}>
|
||||
<p>ITC55100C-WSS ZR</p>
|
||||
</div>
|
||||
<div className={styles['button-grid']}>
|
||||
<ItcButton name="Test Specification" style="b lrg" onClick={() => navigate('testspec')}/>
|
||||
<ItcButton name="Test Modes" style="b lrg" onClick={() => navigate('testmode')}/>
|
||||
<ItcButton name="Package Configure" style="b lrg" onClick={() => navigate('package')}/>
|
||||
<ItcButton name="Test Results" style="b lrg" onClick={() => navigate('testresult')}/>
|
||||
<ItcButton name="Communications" style="b lrg" onClick={() => navigate('communication')}/>
|
||||
<ItcButton name="Settings" style="b lrg" onClick={() => navigate('setting')}/>
|
||||
</div>
|
||||
<div className={styles['hw-config']}>
|
||||
<p>Serial Number: 123456</p>
|
||||
<p>Board Revision: T</p>
|
||||
<p>Crowbar: 200A</p>
|
||||
<p>Sidepanel: 0</p>
|
||||
</div>
|
||||
<div className={styles['copyright']}>
|
||||
<p>© 2026 Integrated Technology Corporation. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import styles from '@/styles/itc/machines/itc55100/panels/testspec.module.css'
|
||||
|
||||
import ItcButton from '@/components/itc/comps/ItcButton'
|
||||
import ItcEntry from '@/components/itc/comps/ItcEntry'
|
||||
import ItcCombo from '@/components/itc/comps/ItcCombo'
|
||||
|
||||
export default function TestSpecPanel({ navigate })
|
||||
{
|
||||
const enModes = ["Disable", "Enable"];
|
||||
const cbModes = ["Disable", "2uS Delay", "5uS Delay", "10uS Delay", "20uS Test Fire"]
|
||||
const lkModes = ["Disable", "Pre", "Post", "Both"];
|
||||
|
||||
return (
|
||||
<div id="itc55100-test-spec-panel" className={styles['container']}>
|
||||
<div className={styles['spec-name']}>
|
||||
<ItcEntry label="Name" value="Test Specification 1" unit=""/>
|
||||
</div>
|
||||
<div className={styles['spec-grid']}>
|
||||
<ItcEntry label="Drain" value="50.0" unit="V"/>
|
||||
<ItcEntry label="Peak" value="10.0" unit="A"/>
|
||||
<ItcEntry label="Inductor" value="1.0" unit="mH"/>
|
||||
<ItcEntry label="Rated Vds" value="250.0" unit="V"/>
|
||||
<ItcEntry label="Channel" value="N" unit=""/>
|
||||
<ItcEntry label="Gate On" value="20.0" unit="V"/>
|
||||
<ItcEntry label="Gate Off" value="0.0" unit="V"/>
|
||||
<ItcCombo label="Leakage" values={lkModes} unit=""/>
|
||||
<ItcEntry label="Leakage" value="50.0" unit="V"/>
|
||||
<ItcEntry label="Leakage" value="1.0" unit="mA"/>
|
||||
<ItcCombo label="Energy" values={enModes} unit=""/>
|
||||
<ItcEntry label="Energy" value="1500.0" unit="mJ"/>
|
||||
<ItcEntry label="T1 Timeout" value="50.0" unit="uS" />
|
||||
<ItcEntry label="T2 Timeout" value="50.0" unit="uS"/>
|
||||
<ItcCombo label="Multipulse" values={enModes} unit=""/>
|
||||
<ItcCombo label="Crowbar" values={cbModes} unit="" disable="true"/>
|
||||
<ItcEntry label="Threshold" value="500.0" unit="V"/>
|
||||
<ItcEntry label="Overvoltage" value="1500.0" unit="V"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
69
node/src/itc/res/itc/machines/itc55100/LeftEdgeCurve.svg
Normal file
@ -0,0 +1,69 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 42 400" width="42" height="400">
|
||||
<defs>
|
||||
<linearGradient id="redGrad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#e8183a"/>
|
||||
<stop offset="40%" stop-color="#c8102e"/>
|
||||
<stop offset="100%" stop-color="#8a0820"/>
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient id="cg" cx="30%" cy="25%" r="65%">
|
||||
<stop offset="0%" stop-color="#777"/>
|
||||
<stop offset="45%" stop-color="#444"/>
|
||||
<stop offset="100%" stop-color="#1a1a1a"/>
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="ch" cx="28%" cy="22%" r="50%">
|
||||
<stop offset="0%" stop-color="#cccccc" stop-opacity="0.55"/>
|
||||
<stop offset="100%" stop-color="#000000" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="rim" cx="30%" cy="25%" r="70%">
|
||||
<stop offset="60%" stop-color="transparent"/>
|
||||
<stop offset="100%" stop-color="#000" stop-opacity="0.6"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<path d="M 0 0 L 13 0 C 13 0, 42 100, 42 200 C 42 300, 13 400, 13 400 L 0 400 Z"
|
||||
fill="url(#redGrad)"/>
|
||||
|
||||
<!-- Circle 1: y=124, r=6, cx=18.6 -->
|
||||
<circle cx="18.6" cy="124" r="7.2" fill="#111"/>
|
||||
<circle cx="18.6" cy="124" r="6" fill="url(#cg)"/>
|
||||
<circle cx="18.6" cy="124" r="6" fill="url(#ch)"/>
|
||||
<circle cx="18.6" cy="124" r="6" fill="url(#rim)"/>
|
||||
<circle cx="18.6" cy="124" r="2.2" fill="#3a3a3a"/>
|
||||
<circle cx="17.2" cy="122.8" r="0.9" fill="#bbb" opacity="0.6"/>
|
||||
|
||||
<!-- Circle 2: y=162, r=8, cx=20.6 -->
|
||||
<circle cx="20.6" cy="162" r="9.5" fill="#111"/>
|
||||
<circle cx="20.6" cy="162" r="8" fill="url(#cg)"/>
|
||||
<circle cx="20.6" cy="162" r="8" fill="url(#ch)"/>
|
||||
<circle cx="20.6" cy="162" r="8" fill="url(#rim)"/>
|
||||
<circle cx="20.6" cy="162" r="3" fill="#3a3a3a"/>
|
||||
<circle cx="19.0" cy="160.6" r="1.2" fill="#bbb" opacity="0.6"/>
|
||||
|
||||
<!-- Circle 3: y=200, r=10, cx=22.6 -->
|
||||
<circle cx="22.6" cy="200" r="11.8" fill="#111"/>
|
||||
<circle cx="22.6" cy="200" r="10" fill="url(#cg)"/>
|
||||
<circle cx="22.6" cy="200" r="10" fill="url(#ch)"/>
|
||||
<circle cx="22.6" cy="200" r="10" fill="url(#rim)"/>
|
||||
<circle cx="22.6" cy="200" r="3.8" fill="#3a3a3a"/>
|
||||
<circle cx="20.8" cy="198.4" r="1.5" fill="#bbb" opacity="0.6"/>
|
||||
|
||||
<!-- Circle 4: y=238, r=8, cx=20.6 -->
|
||||
<circle cx="20.6" cy="238" r="9.5" fill="#111"/>
|
||||
<circle cx="20.6" cy="238" r="8" fill="url(#cg)"/>
|
||||
<circle cx="20.6" cy="238" r="8" fill="url(#ch)"/>
|
||||
<circle cx="20.6" cy="238" r="8" fill="url(#rim)"/>
|
||||
<circle cx="20.6" cy="238" r="3" fill="#3a3a3a"/>
|
||||
<circle cx="19.0" cy="236.6" r="1.2" fill="#bbb" opacity="0.6"/>
|
||||
|
||||
<!-- Circle 5: y=276, r=6, cx=18.6 -->
|
||||
<circle cx="18.6" cy="276" r="7.2" fill="#111"/>
|
||||
<circle cx="18.6" cy="276" r="6" fill="url(#cg)"/>
|
||||
<circle cx="18.6" cy="276" r="6" fill="url(#ch)"/>
|
||||
<circle cx="18.6" cy="276" r="6" fill="url(#rim)"/>
|
||||
<circle cx="18.6" cy="276" r="2.2" fill="#3a3a3a"/>
|
||||
<circle cx="17.2" cy="274.8" r="0.9" fill="#bbb" opacity="0.6"/>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
61
node/src/itc/styles/itc/comps/itc-button.css
Normal file
@ -0,0 +1,61 @@
|
||||
#itc-button-container
|
||||
{
|
||||
--button-width: 300px;
|
||||
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 5px rgb(255,255,255);
|
||||
}
|
||||
|
||||
#itc-button-container button
|
||||
{
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--text-size-small);
|
||||
padding: 0.5rem 0.5rem;
|
||||
border-radius: var(--border-radius-medium);
|
||||
border-width: 1px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
#itc-button-container button:hover
|
||||
{
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
#itc-button-container button:active
|
||||
{
|
||||
background-color: var(--color-secondary-light);
|
||||
}
|
||||
|
||||
#itc-button-container button.a
|
||||
{
|
||||
font-size: var(--text-size-large);
|
||||
}
|
||||
|
||||
#itc-button-container button.b
|
||||
{
|
||||
font-size: var(--text-size-medium);
|
||||
}
|
||||
|
||||
#itc-button-container button.c
|
||||
{
|
||||
font-size: var(--text-size-small);
|
||||
}
|
||||
|
||||
#itc-button-container button.lrg
|
||||
{
|
||||
width: calc(var(--button-width)*1);
|
||||
}
|
||||
|
||||
#itc-button-container button.med
|
||||
{
|
||||
width: calc(var(--button-width)*.75);
|
||||
}
|
||||
|
||||
#itc-button-container button.sm
|
||||
{
|
||||
width: calc(var(--button-width)*.50);
|
||||
}
|
||||
|
||||
24
node/src/itc/styles/itc/comps/itc-combo.module.css
Normal file
@ -0,0 +1,24 @@
|
||||
.container
|
||||
{
|
||||
display: flex;
|
||||
flex: row;
|
||||
font-size: var(--text-size-medium);
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.container .label-container
|
||||
{
|
||||
display: flex;
|
||||
width: 125px;
|
||||
justify-content: right;
|
||||
}
|
||||
.container .entry-container select
|
||||
{
|
||||
font-size: var(--text-size-medium);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.disabled
|
||||
{
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
24
node/src/itc/styles/itc/comps/itc-entry.module.css
Normal file
@ -0,0 +1,24 @@
|
||||
.container
|
||||
{
|
||||
display: flex;
|
||||
flex: row;
|
||||
font-size: var(--text-size-medium);
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.container .label-container
|
||||
{
|
||||
display: flex;
|
||||
width: 125px;
|
||||
justify-content: right;
|
||||
}
|
||||
.container .entry-container input
|
||||
{
|
||||
font-size: var(--text-size-medium);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.disabled
|
||||
{
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
0
node/src/itc/styles/itc/comps/itc-keyboard.css
Normal file
33
node/src/itc/styles/itc/machines/itc-machine.css
Normal file
@ -0,0 +1,33 @@
|
||||
#itc-machine-container
|
||||
{
|
||||
--font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
--text-size-small: 1rem;
|
||||
--text-size-medium: 1.25rem;
|
||||
--text-size-large: 2rem;
|
||||
|
||||
--color-primary-light: #e8183a;
|
||||
--color-primary: #c8102e;
|
||||
--color-primary-dark: #8a0820;
|
||||
|
||||
--color-secondary-light: #bfbfbf;
|
||||
--color-secondary:#838383;
|
||||
--color-secondary-dark: #535353;
|
||||
|
||||
--border-radius-medium: 10px;
|
||||
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--text-size-large);
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.outline
|
||||
{
|
||||
border: 1px solid #3498db;
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
.container {
|
||||
--panel-buffer: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: linear-gradient(180deg, rgb(230, 230, 230) 0%, var(--color-secondary-light) 2%, var(--color-secondary) 98%, var(--color-secondary-dark) 100%);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0px 51px 0px 0px;
|
||||
}
|
||||
|
||||
.left-edge {
|
||||
position: relative;
|
||||
width: 51px;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: var(--panel-buffer) 0px var(--panel-buffer) 0px;
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
.container
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.button-grid
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.info-bar
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
font-size: var(--text-size-small);
|
||||
}
|
||||
|
||||
.title-bar
|
||||
{
|
||||
display: flex;
|
||||
border-bottom: 1px solid #000000;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
font-size: var(--text-size-large);
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.hw-config
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: space-around;
|
||||
gap: 1rem;
|
||||
font-size: var(--text-size-small);
|
||||
}
|
||||
|
||||
.copyright
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: var(--text-size-small);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
.container
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.container .spec-name
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container .spec-grid
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
5
node/src/lib/auth.js
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET)
|
||||
const ADMIN_ROLE_ID = 1
|
||||
|
||||
export { SECRET, ADMIN_ROLE_ID }
|
||||
69
node/src/lib/db/UserService.js
Normal file
@ -0,0 +1,69 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
import { Query } from '@/lib/db/db'
|
||||
|
||||
export async function addUser(fname, lname, username, email, password) {
|
||||
|
||||
username = username.toLowerCase().trim()
|
||||
email = email.toLowerCase().trim()
|
||||
|
||||
const existingUser = await getUserByUsername(username)
|
||||
if (existingUser) {
|
||||
throw new Error('USERNAME_EXISTS')
|
||||
}
|
||||
|
||||
const existingEmail = await getUserByEmail(email)
|
||||
if (existingEmail) {
|
||||
throw new Error('EMAIL_EXISTS')
|
||||
}
|
||||
|
||||
const status = 1; // active
|
||||
const role = 4;
|
||||
const hash = await bcrypt.hash(password, 10)
|
||||
const result = await Query(
|
||||
`INSERT INTO users (first_name, last_name, username, email, password_hash, role_id, status_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, first_name, last_name, username, email, password_hash, role_id, status_id`,
|
||||
[fname, lname, username, email, hash, role, status]
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getUserByUsername(username) {
|
||||
username = username.toLowerCase().trim()
|
||||
const result = await Query(
|
||||
`SELECT id, first_name, last_name, username, email, password_hash, role_id, status_id
|
||||
FROM users
|
||||
WHERE username = $1`,
|
||||
[username]
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getUserByEmail(email) {
|
||||
email = email.toLowerCase().trim()
|
||||
const result = await Query(
|
||||
`SELECT id, first_name, last_name, username, email, password_hash, role_id, status_id
|
||||
FROM users
|
||||
WHERE email = $1`,
|
||||
[email]
|
||||
)
|
||||
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function loginUser(usernameOrEmail, password) {
|
||||
const user = await getUserByUsername(usernameOrEmail) || await getUserByEmail(usernameOrEmail)
|
||||
if (!user) {
|
||||
throw new Error('USER_NOT_FOUND')
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.password_hash)
|
||||
if (!isMatch) {
|
||||
throw new Error('INVALID_CREDENTIALS')
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
15
node/src/lib/db/db.js
Normal file
@ -0,0 +1,15 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import pg from 'pg'
|
||||
|
||||
const { Pool } = pg
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
})
|
||||
|
||||
export { pool }
|
||||
|
||||
export async function Query(query, params = []) {
|
||||
const result = await pool.query(query, params)
|
||||
return result
|
||||
}
|
||||
62
node/src/proxy.js
Normal file
@ -0,0 +1,62 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { jwtVerify } from 'jose';
|
||||
import { SECRET } from '@/lib/auth';
|
||||
import { ADMIN_ROLE_ID } from '@/lib/auth';
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon\\.ico|public/).*)'
|
||||
]
|
||||
}
|
||||
|
||||
export default async function proxy(request) {
|
||||
const { pathname } = request.nextUrl
|
||||
const token = request.cookies.get('session')?.value
|
||||
|
||||
|
||||
|
||||
const publicRoutes = ['/login', '/signup', '/shop']
|
||||
if (publicRoutes.some(route => pathname.startsWith(route)) ) {
|
||||
if (token) {
|
||||
return NextResponse.redirect(new URL('/', request.url))
|
||||
}
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
const publicApis = ['/api/public']
|
||||
if (publicApis.some(route => pathname.startsWith(route)) ) {
|
||||
if (token) {
|
||||
return NextResponse.json({ message: 'Already authenticated' }, { status: 403 })
|
||||
}
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
|
||||
try {
|
||||
const adminId = ADMIN_ROLE_ID
|
||||
|
||||
const { payload } = await jwtVerify(token, SECRET)
|
||||
|
||||
const adminRoutes = ['/admin']
|
||||
if (adminRoutes.some(route => pathname.startsWith(route))) {
|
||||
if (payload.roleId !== adminId) {
|
||||
return NextResponse.redirect(new URL('/', request.url))
|
||||
}
|
||||
}
|
||||
|
||||
const adminApis = ['/api/admin']
|
||||
if (adminApis.some(route => pathname.startsWith(route))) {
|
||||
if (payload.roleId !== adminId) {
|
||||
return NextResponse.json({ message: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
|
||||
} catch (err) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
}
|
||||
9
node/src/res/rts_logo.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg viewBox="0 0 126 144" role="img" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(63, 72)">
|
||||
<polygon points="0,-66 57,-33 57,33 0,66 -57,33 -57,-33" fill="#273b6b"/>
|
||||
<polygon points="0,-54 46,-27 46,27 0,54 -46,27 -46,-27" fill="#1e40af"/>
|
||||
<polygon points="0,-54 46,-27 46,27 0,54 -46,27 -46,-27" fill="none" stroke="#3370d1" stroke-width="1" opacity="0.5"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" y="1" style="fill:#ffffff;font-family:system-ui,sans-serif;font-size:32px;font-weight:500;">RTS</text>
|
||||
<rect x="-26" y="42" width="52" height="2" rx="1" fill="#dbeafe" opacity="0.35"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 650 B |
24
node/src/res/rts_logo_horizontal.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<svg width="100%" viewBox="0 0 680 200" role="img" xmlns="http://www.w3.org/2000/svg" style="">
|
||||
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">RynoTech Services horizontal logo</title>
|
||||
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Horizontal layout logo for RynoTech Services with hexagonal RTS mark on the left and stacked wordmark on the right.</desc>
|
||||
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
|
||||
<!-- ── hex mark, centered vertically at y=100 ── -->
|
||||
<g transform="translate(180,100)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<polygon points="0,-66 57,-33 57,33 0,66 -57,33 -57,-33" fill="#273b6b" style="fill:rgb(39, 59, 107);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<polygon points="0,-54 46,-27 46,27 0,54 -46,27 -46,-27" fill="#1e40af" style="fill:rgb(30, 64, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<polygon points="0,-54 46,-27 46,27 0,54 -46,27 -46,-27" fill="none" stroke="#3370d1" stroke-width="1" opacity="0.5" style="fill:none;stroke:rgb(51, 112, 209);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.5;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" y="1" style="fill:rgb(255, 255, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", system-ui, sans-serif;font-size:32px;font-weight:500;text-anchor:middle;dominant-baseline:central">RTS</text>
|
||||
<rect x="-26" y="42" width="52" height="2" rx="1" fill="#dbeafe" opacity="0.35" style="fill:rgb(219, 234, 254);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.35;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
</g>
|
||||
|
||||
<!-- ── vertical divider ── -->
|
||||
<line x1="258" y1="60" x2="258" y2="140" stroke="#3370d1" stroke-width="1" opacity="0.3" style="fill:rgb(0, 0, 0);stroke:rgb(51, 112, 209);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.3;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- ── wordmark, vertically centered ── -->
|
||||
<text x="278" y="96" style="fill:rgb(30, 64, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", system-ui, sans-serif;font-size:28px;font-weight:500;text-anchor:start;dominant-baseline:auto">RYNOTECH</text>
|
||||
<text x="280" y="118" style="fill:rgb(51, 112, 209);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", system-ui, sans-serif;font-size:11.5px;font-weight:400;text-anchor:start;dominant-baseline:auto">SERVICES</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
48
node/src/styles/Layout.module.css
Normal file
@ -0,0 +1,48 @@
|
||||
.container {
|
||||
--logo-height:80px;
|
||||
--logo-width:80px;
|
||||
|
||||
--navbar-height: 40px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
height: var(--logo-height);
|
||||
min-height: var(--logo-height);
|
||||
background-color: var(--secondary-color-highlight);
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
min-width: var(--logo-width);
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.userbar-container {
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.content-container
|
||||
{
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
background-color: var(--secondary-color-highlight);
|
||||
border-radius: 10px;
|
||||
}
|
||||
98
node/src/styles/card.css
Normal file
@ -0,0 +1,98 @@
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-bg-color);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.card-header h1 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.card-header p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--secondary-color-highlight);
|
||||
padding: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.card-entry label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-entry input {
|
||||
padding: 1rem;
|
||||
border-radius: 50px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-actions label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-bg-color);
|
||||
padding: 1rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
min-width: 125px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-bg-color);
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.card-footer p {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.card-footer a {
|
||||
color: var(--text-bg-color);
|
||||
}
|
||||
|
||||
.card-error {
|
||||
color: var(--danger-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
88
node/src/styles/global.css
Normal file
@ -0,0 +1,88 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
:root
|
||||
{
|
||||
--text-fg-color: #171717;
|
||||
--text-bg-color: #ffffff;
|
||||
|
||||
/* Primary — navy/blue, main actions and branding */
|
||||
--primary-color-dark: #273b6b;
|
||||
--primary-color: #1e40af;
|
||||
--primary-color-highlight: #3370d1;
|
||||
--primary-color-subtle: #dbeafe;
|
||||
|
||||
/* Secondary — slate, UI chrome, borders, inactive elements */
|
||||
--secondary-color-dark: #1e293b;
|
||||
--secondary-color: #64748b;
|
||||
--secondary-color-highlight:#94a3b8;
|
||||
--secondary-color-subtle: #f8fafc;
|
||||
|
||||
/* Surface — backgrounds, cards, panels */
|
||||
--surface-color-base: #0f172a;
|
||||
--surface-color-raised: #1e293b;
|
||||
--surface-color-overlay: #334155;
|
||||
--surface-color-border: #475569;
|
||||
|
||||
--background-color: #ffffff;
|
||||
|
||||
/* Text */
|
||||
--text-color-primary: #f8fafc;
|
||||
--text-color-secondary: #94a3b8;
|
||||
--text-color-muted: #64748b;
|
||||
--text-color-inverse: #0f172a;
|
||||
|
||||
/* Success — stock received, work order complete, ECO approved */
|
||||
--success-color-dark: #14532d;
|
||||
--success-color: #16a34a;
|
||||
--success-color-highlight: #4ade80;
|
||||
--success-color-subtle: #dcfce7;
|
||||
|
||||
/* Warning — low stock, reorder point, calibration due */
|
||||
--warning-color-dark: #78350f;
|
||||
--warning-color: #d97706;
|
||||
--warning-color-highlight: #fbbf24;
|
||||
--warning-color-subtle: #fef3c7;
|
||||
|
||||
/* Danger — obsolete profile, overdue PO, reservation conflict */
|
||||
--danger-color-dark: #7f1d1d;
|
||||
--danger-color: #dc2626;
|
||||
--danger-color-highlight: #f87171;
|
||||
--danger-color-subtle: #fee2e2;
|
||||
|
||||
/* Info — general alerts, ECO pending, notifications */
|
||||
--info-color-dark: #1e3a5f;
|
||||
--info-color: #0284c7;
|
||||
--info-color-highlight: #38bdf8;
|
||||
--info-color-subtle: #e0f2fe;
|
||||
|
||||
--font-family:Tahoma,system-ui,sans-serif;
|
||||
|
||||
--text-size-small: 0.875rem;
|
||||
--text-size-medium: 1rem;
|
||||
--text-size-large: 1.25rem;
|
||||
|
||||
--no-border: none;
|
||||
--on-border: 1px solid red;
|
||||
--debug-border: var(--on-border);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--text-size-medium);
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-fg-color);
|
||||
height: 100vh;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.center-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
7
node/src/styles/pages/Dashboard.module.css
Normal file
@ -0,0 +1,7 @@
|
||||
.container
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto auto;
|
||||
height: 100%;
|
||||
}
|
||||
6
node/src/styles/pages/Main.module.css
Normal file
@ -0,0 +1,6 @@
|
||||
.container
|
||||
{
|
||||
border: var(--debug-border);
|
||||
height: 100%;
|
||||
background-color: var(--secondary-color-highlight);
|
||||
}
|
||||
101
node/src/styles/pages/login.css
Normal file
@ -0,0 +1,101 @@
|
||||
/* Login Page */
|
||||
#login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-bg-color);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--secondary-color-highlight);
|
||||
padding: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.login-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.login-entry label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.login-entry input {
|
||||
padding: 1rem;
|
||||
border-radius: 50px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.login-actions label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-bg-color);
|
||||
padding: 1rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-bg-color);
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.login-footer a {
|
||||
color: var(--text-bg-color);
|
||||
}
|
||||
|
||||
15
rebuild.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
COMPOSE_DIR="./"
|
||||
CACHE_VOLUME="app_nextjs_cache"
|
||||
|
||||
echo "Bringing down containers..."
|
||||
cd $COMPOSE_DIR && docker compose down
|
||||
|
||||
echo "Removing Next.js cache volume..."
|
||||
docker volume rm $CACHE_VOLUME
|
||||
|
||||
echo "Bringing containers back up..."
|
||||
docker compose up -d
|
||||
|
||||
echo "Done."
|
||||