diff --git a/README.md b/README.md index 1751986..aab2a15 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,28 @@ **Werkzeugkiste** is a Next.js-based website that offers a collection of useful digital tools and converters. This platform is designed to provide users with a simple and efficient way to handle various tasks, such as converting -files, calculating values, or using other handy digital utilities. +files, generate content, or using other handy digital utilities. This page is made by two persons and privacy-focused. ## Tech-Stack -- **Next.js**: React framework for server-side rendering and static websites. -- **React**: JavaScript library for building user interfaces. +- **Next.js**: React framework for our frontend with server-side rendering and static content. +- **React**: JavaScript library for building our user interfaces. - **TypeScript**: Typed JavaScript superset for improved code quality. - **Tailwind CSS**: Utility-first CSS framework for styling. -- **Docker**: Containerization platform for deployment. +- **Fastify**: Low overhead NodeJS framework for our backend. +- **Docker**: Containerization platform for deploying frontend and backend. ## Implemented Tools -- **File Converter**: Convert files between different formats. -- **Image Converter**: Convert images between different formats. -- **Color Converter**: Convert colors between different formats. -- **Password Generator**: Generate secure passwords. -- **Pomodoro Timer**: Use the Pomodoro technique to manage time. -- **QR Code Generator**: Generate QR codes for URLs, text, or other data. +- **doc-converter**: Convert documents between different formats. +- **img-converter**: Convert images between different formats. +- **rgb-to-hex**: Convert colors between different formats. +- **data-visualizer**: Visualize your data from table to chart. +- **qr-code-generator**: Generate QR codes for URLs, text, or other data. +- **password-generator**: Generate secure passwords. +- **bg-remover**: Remove backgrounds from your images. +- **word-counter**: Count words of your documents. +- **pomodoro-timer**: Use the Pomodoro technique to manage time. ## Installation diff --git a/backend/.prettierignore b/backend/.prettierignore new file mode 100644 index 0000000..1b8ac88 --- /dev/null +++ b/backend/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6583c94 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,54 @@ +# Basis-Image +FROM node:18-alpine AS base + +# Install system dependencies +RUN apk add --no-cache libc6-compat libreoffice ttf-liberation + +WORKDIR /app + +# Dependencies installieren +FROM base AS deps +COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ + +RUN \ + if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Build stage +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . /app + +RUN \ + if [ -f yarn.lock ]; then yarn run build; \ + elif [ -f package-lock.json ]; then npm run build; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Production image +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 fastify + +# Copy built application +COPY --from=builder --chown=fastify:nodejs /app/dist ./dist +COPY --from=deps --chown=fastify:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=fastify:nodejs /app/package.json ./package.json + +USER fastify + +EXPOSE 4000 + +ENV PORT=4000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "dist/server.js"] \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index cb04950..8c99059 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@types/node": "^22.13.4", + "prettier": "3.5.1", "ts-node": "^10.9.2", "typescript": "^5.7.3" } @@ -758,6 +759,22 @@ "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", "license": "MIT" }, + "node_modules/prettier": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-warning": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index fbb889d..671e0b5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,6 +22,7 @@ }, "devDependencies": { "@types/node": "^22.13.4", + "prettier": "3.5.1", "ts-node": "^10.9.2", "typescript": "^5.7.3" } diff --git a/backend/server.ts b/backend/server.ts index 5156540..5deaac0 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -1,15 +1,17 @@ import Fastify from "fastify"; import cors from "@fastify/cors"; -import multipart from '@fastify/multipart'; +import multipart from "@fastify/multipart"; import { libreConvert } from "./src/routes/libreconvert.route"; +import { colorConvert } from "./src/routes/colorconvert.route"; const app = Fastify({ logger: true }); app.register(cors, { origin: "*", exposedHeaders: 'Content-Disposition' }); app.register(multipart); app.register(libreConvert); +app.register(colorConvert); const PORT = process.env.PORT || 4000; app.listen({ port: Number(PORT), host: "0.0.0.0" }, () => { - console.log(`🚀Fastify is live on http://localhost:${PORT}`); -}); \ No newline at end of file + console.log(`🚀Fastify is live on http://localhost:${PORT}`); +}); diff --git a/backend/src/routes/colorconvert.route.ts b/backend/src/routes/colorconvert.route.ts new file mode 100644 index 0000000..402e642 --- /dev/null +++ b/backend/src/routes/colorconvert.route.ts @@ -0,0 +1,29 @@ +import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; + +interface RequestBody { + red: string; + green: string; + blue: string; +} + +export async function colorConvert(app: FastifyInstance) { + app.post( + "/api/color-convert", + async ( + request: FastifyRequest<{ Body: RequestBody }>, + reply: FastifyReply, + ) => { + try { + const data = request.body; + if (!data) { + return reply.status(400).send({ error: "No RGB declared!" }); + } + const hex = (`#${(+data.red).toString(16).padStart(2, "0")}${(+data.green).toString(16).padStart(2, "0")}${(+data.blue).toString(16).padStart(2, "0")}`).toUpperCase(); + reply.header("Content-Type", "application/json").send({ hex: hex }); + } catch (error) { + console.error("Convert error:", error); + reply.status(500).send({ error: "Error while converting!" }); + } + }, + ); +} diff --git a/backend/src/routes/libreconvert.route.ts b/backend/src/routes/libreconvert.route.ts index 746be93..0d726ac 100644 --- a/backend/src/routes/libreconvert.route.ts +++ b/backend/src/routes/libreconvert.route.ts @@ -62,4 +62,4 @@ export async function libreConvert(app: FastifyInstance) { reply.status(500).send({ error: "Error while converting!" }); } }); -} \ No newline at end of file +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 623a8fe..2ef8988 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES6", "module": "CommonJS", "outDir": "./dist", - "rootDir": "./src", + "rootDir": ".", "strict": true, "allowSyntheticDefaultImports": true } diff --git a/docker-compose.yaml b/docker-compose.yaml index cc0e49a..f3e968d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,13 +1,32 @@ services: - app: + frontend: build: - context: . + context: ./frontend dockerfile: Dockerfile - container_name: werkzeugkiste + container_name: werkzeugkiste-frontend environment: - NODE_ENV: production - HOSTNAME: 0.0.0.0 - PORT: 3000 + - NODE_ENV=production + - HOSTNAME=0.0.0.0 + - PORT=3000 + - backend_url=http://backend:4000 ports: - "3000:3000" restart: unless-stopped + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: werkzeugkiste-backend + environment: + - NODE_ENV=production + - HOSTNAME=0.0.0.0 + - PORT=4000 + - CORS_ORIGIN=http://frontend:3000 + ports: + - "4000:4000" + restart: unless-stopped + +networks: + default: + driver: bridge \ No newline at end of file diff --git a/Dockerfile b/frontend/Dockerfile similarity index 75% rename from Dockerfile rename to frontend/Dockerfile index d5e904c..4d67048 100644 --- a/Dockerfile +++ b/frontend/Dockerfile @@ -5,11 +5,11 @@ FROM node:18-alpine AS base # Install dependencies only when needed FROM base AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat libreoffice ttf-liberation +RUN apk add --no-cache libc6-compat WORKDIR /app # Install dependencies based on the preferred package manager -COPY frontend/package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ RUN \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ @@ -47,13 +47,18 @@ ENV NODE_ENV=production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static +# 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 USER nextjs EXPOSE 3000 -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" +ENV PORT=3000 + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output +ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"] \ No newline at end of file diff --git a/frontend/src/app/rgb-to-hex/layout.tsx b/frontend/src/app/rgb-to-hex/layout.tsx new file mode 100644 index 0000000..04c7606 --- /dev/null +++ b/frontend/src/app/rgb-to-hex/layout.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import type { Metadata } from "next"; +import { toolLinks } from "@/constants"; + +export const metadata: Metadata = { + title: toolLinks[2].title, + description: "Converter for rgb to hex color format!", +}; + +export default function RootLayout({ + children, + }: Readonly<{ + children: React.ReactNode; +}>) { + return ( + +
{children} + + ); +} \ No newline at end of file diff --git a/frontend/src/app/rgb-to-hex/page.tsx b/frontend/src/app/rgb-to-hex/page.tsx new file mode 100644 index 0000000..cdef0e4 --- /dev/null +++ b/frontend/src/app/rgb-to-hex/page.tsx @@ -0,0 +1,133 @@ +"use client"; + +import React, { useState } from "react"; +import Navbar from "../../components/Navbar"; +import Footer from "../../components/Footer"; +import Button from "../../components/Button"; + +export default function RgbToHex() { + + const [loading, setLoading] = useState(false); + const [hex, setHex] = useState(""); + + const convertToHex = async () => { + + setLoading(true); + + const red = (document.getElementById("red") as HTMLInputElement).value; + const green = (document.getElementById("green") as HTMLInputElement).value; + const blue = (document.getElementById("blue") as HTMLInputElement).value; + + try { + const response = await fetch( + process.env.backend_url + "/api/color-convert", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ red: red, green: green, blue: blue }), + }, + ); + if (!response.ok) { + return new Error(`Error: ${response.statusText}`); + } + const hex: string = await response.text(); + setHex(hex); + + } catch (error) { + console.error("Error while converting:", error); + alert("Error while converting"); + } finally { + setLoading(false); + } + }; + + const clearInAndOutput = () => { + setHex(""); + const red = document.getElementById("red") as HTMLInputElement; + const green = document.getElementById("green") as HTMLInputElement; + const blue = document.getElementById("blue") as HTMLInputElement; + red.value = ""; + green.value = ""; + blue.value = ""; + }; + + const checkInput = (event: React.ChangeEvent