From de83c5f5d6374ee362f5657c69a6ae6ae05be37f Mon Sep 17 00:00:00 2001 From: Theo Leuthardt <60556192+theoleuthardt@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:56:20 +0100 Subject: [PATCH 1/4] updated tool list --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1751986..64adaea 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **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 @@ -14,12 +14,15 @@ files, calculating values, or using other handy digital utilities. ## 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. +- **File-To-PDF**: Convert files between different formats. +- **IMG-To-PNG**: Convert images between different formats. +- **RGB-To-HEX**: Convert colors between different formats. +- **Video-To-GIF**: Convert your video files to gifs. - **QR Code Generator**: Generate QR codes for URLs, text, or other data. +- **Password Generator**: Generate secure passwords. +- **Background-Remover**: Remove backgrounds from your images. +- **Word-Counter**: Count words of your documents. +- **Pomodoro Timer**: Use the Pomodoro technique to manage time. ## Installation From be3b9d7f5d43bcba8488eaf8bad942ae73ca0fcf Mon Sep 17 00:00:00 2001 From: Theo Leuthardt <60556192+theoleuthardt@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:08:27 +0100 Subject: [PATCH 2/4] new tool ideas Now our tool list is complete and these tools will be implemented! --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 64adaea..aab2a15 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,24 @@ files, generate content, or using other handy digital utilities. This page is ma ## 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-To-PDF**: Convert files between different formats. -- **IMG-To-PNG**: Convert images between different formats. -- **RGB-To-HEX**: Convert colors between different formats. -- **Video-To-GIF**: Convert your video files to gifs. -- **QR Code Generator**: Generate QR codes for URLs, text, or other data. -- **Password Generator**: Generate secure passwords. -- **Background-Remover**: Remove backgrounds from your images. -- **Word-Counter**: Count words of your documents. -- **Pomodoro Timer**: Use the Pomodoro technique to manage time. +- **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 From be3ab2bcaf1159fdd8a9155189f81219adad054e Mon Sep 17 00:00:00 2001 From: theoleuthardt Date: Sat, 15 Feb 2025 11:19:44 +0100 Subject: [PATCH 3/4] feat: new fixed Dockerfiles for deployment in separate folders, docker compose for composing the stack --- backend/Dockerfile | 54 +++++++++++++++++++++++++++++++ backend/tsconfig.json | 2 +- docker-compose.yaml | 31 ++++++++++++++---- Dockerfile => frontend/Dockerfile | 17 ++++++---- 4 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 backend/Dockerfile rename Dockerfile => frontend/Dockerfile (75%) 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/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 From 557efd74ba83f92dabda06e561a7b76fa638d336 Mon Sep 17 00:00:00 2001 From: Domenik Date: Sat, 15 Feb 2025 19:52:49 +0100 Subject: [PATCH 4/4] rgb-to-hex converter fully implemented --- backend/.prettierignore | 3 + backend/.prettierrc | 1 + backend/package-lock.json | 17 +++ backend/package.json | 1 + backend/server.ts | 8 +- backend/src/routes/colorconvert.route.ts | 29 +++++ backend/src/routes/libreconvert.route.ts | 35 +++--- frontend/src/app/rgb-to-hex/layout.tsx | 20 ++++ frontend/src/app/rgb-to-hex/page.tsx | 133 +++++++++++++++++++++++ 9 files changed, 228 insertions(+), 19 deletions(-) create mode 100644 backend/.prettierignore create mode 100644 backend/.prettierrc create mode 100644 backend/src/routes/colorconvert.route.ts create mode 100644 frontend/src/app/rgb-to-hex/layout.tsx create mode 100644 frontend/src/app/rgb-to-hex/page.tsx 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/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 03bb2e9..220d0e3 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: "*" }); 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 ac4c8af..d28705e 100644 --- a/backend/src/routes/libreconvert.route.ts +++ b/backend/src/routes/libreconvert.route.ts @@ -5,26 +5,29 @@ import { promisify } from "util"; const libreConvertAsync = promisify(libre.convert); export async function libreConvert(app: FastifyInstance) { - app.post("/api/libre-convert", async (request: FastifyRequest, reply: FastifyReply) => { - try { - const data = await request.file(); - if (!data) { - return reply.status(400).send({ error: "No file uploaded!" }); - } + app.post( + "/api/libre-convert", + async (request: FastifyRequest, reply: FastifyReply) => { + try { + const data = await request.file(); + if (!data) { + return reply.status(400).send({ error: "No file uploaded!" }); + } - const ext = ".pdf"; - const format = ext.substring(1); - const fileBuffer = await data.toBuffer(); + const ext = ".pdf"; + const format = ext.substring(1); + const fileBuffer = await data.toBuffer(); - const pdfBuffer = await libreConvertAsync(fileBuffer, ext, undefined); + const pdfBuffer = await libreConvertAsync(fileBuffer, ext, undefined); - reply + reply .header("Content-Type", "application/" + format) .header("Content-Disposition", "attachment; filename=converted" + ext) .send(pdfBuffer); - } catch (error) { - console.error("Convert error:", error); - reply.status(500).send({ error: "Error while converting!" }); - } - }); + } catch (error) { + console.error("Convert error:", error); + reply.status(500).send({ error: "Error while converting!" }); + } + }, + ); } 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) => { + const color = (document.getElementById(event.target.id) as HTMLInputElement); + const colorValue = +color.value; + + if (colorValue < 0 || colorValue > 255 ) { + alert("Invalid input. Please enter a number between 0 and 255."); + } + if (colorValue < 0) { + color.value = "0"; + } else if (colorValue > 255) { + color.value = "255"; + } + } + + return ( +
+ +
+

rgb-to-hex

+
+ + + + + + +
+
+
+
+ {hex} +
+
+
+ ); +} \ No newline at end of file