Merge branch 'main' into feat/doc-converter

This commit is contained in:
Theo Leuthardt 2025-02-18 10:18:09 +01:00 committed by GitHub
commit 8fe6657200
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 315 additions and 27 deletions

View file

@ -2,24 +2,28 @@
**Werkzeugkiste** is a Next.js-based website that offers a collection of useful digital tools and converters. **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 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 ## Tech-Stack
- **Next.js**: React framework for server-side rendering and static websites. - **Next.js**: React framework for our frontend with server-side rendering and static content.
- **React**: JavaScript library for building user interfaces. - **React**: JavaScript library for building our user interfaces.
- **TypeScript**: Typed JavaScript superset for improved code quality. - **TypeScript**: Typed JavaScript superset for improved code quality.
- **Tailwind CSS**: Utility-first CSS framework for styling. - **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 ## Implemented Tools
- **File Converter**: Convert files between different formats. - **doc-converter**: Convert documents between different formats.
- **Image Converter**: Convert images between different formats. - **img-converter**: Convert images between different formats.
- **Color Converter**: Convert colors between different formats. - **rgb-to-hex**: Convert colors between different formats.
- **Password Generator**: Generate secure passwords. - **data-visualizer**: Visualize your data from table to chart.
- **Pomodoro Timer**: Use the Pomodoro technique to manage time. - **qr-code-generator**: Generate QR codes for URLs, text, or other data.
- **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 ## Installation

3
backend/.prettierignore Normal file
View file

@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

1
backend/.prettierrc Normal file
View file

@ -0,0 +1 @@
{}

54
backend/Dockerfile Normal file
View file

@ -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"]

View file

@ -19,6 +19,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"prettier": "3.5.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
@ -758,6 +759,22 @@
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT" "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": { "node_modules/process-warning": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",

View file

@ -22,6 +22,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"prettier": "3.5.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }

View file

@ -1,15 +1,17 @@
import Fastify from "fastify"; import Fastify from "fastify";
import cors from "@fastify/cors"; import cors from "@fastify/cors";
import multipart from '@fastify/multipart'; import multipart from "@fastify/multipart";
import { libreConvert } from "./src/routes/libreconvert.route"; import { libreConvert } from "./src/routes/libreconvert.route";
import { colorConvert } from "./src/routes/colorconvert.route";
const app = Fastify({ logger: true }); const app = Fastify({ logger: true });
app.register(cors, { origin: "*", exposedHeaders: 'Content-Disposition' }); app.register(cors, { origin: "*", exposedHeaders: 'Content-Disposition' });
app.register(multipart); app.register(multipart);
app.register(libreConvert); app.register(libreConvert);
app.register(colorConvert);
const PORT = process.env.PORT || 4000; const PORT = process.env.PORT || 4000;
app.listen({ port: Number(PORT), host: "0.0.0.0" }, () => { app.listen({ port: Number(PORT), host: "0.0.0.0" }, () => {
console.log(`🚀Fastify is live on http://localhost:${PORT}`); console.log(`🚀Fastify is live on http://localhost:${PORT}`);
}); });

View file

@ -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!" });
}
},
);
}

View file

@ -62,4 +62,4 @@ export async function libreConvert(app: FastifyInstance) {
reply.status(500).send({ error: "Error while converting!" }); reply.status(500).send({ error: "Error while converting!" });
} }
}); });
} }

View file

@ -3,7 +3,7 @@
"target": "ES6", "target": "ES6",
"module": "CommonJS", "module": "CommonJS",
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": ".",
"strict": true, "strict": true,
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
} }

View file

@ -1,13 +1,32 @@
services: services:
app: frontend:
build: build:
context: . context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: werkzeugkiste container_name: werkzeugkiste-frontend
environment: environment:
NODE_ENV: production - NODE_ENV=production
HOSTNAME: 0.0.0.0 - HOSTNAME=0.0.0.0
PORT: 3000 - PORT=3000
- backend_url=http://backend:4000
ports: ports:
- "3000:3000" - "3000:3000"
restart: unless-stopped 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

View file

@ -5,11 +5,11 @@ FROM node:18-alpine AS base
# Install dependencies only when needed # Install dependencies only when needed
FROM base AS deps FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # 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 WORKDIR /app
# Install dependencies based on the preferred package manager # 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 \ RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \ elif [ -f package-lock.json ]; then npm ci; \
@ -47,13 +47,18 @@ ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/.next/standalone ./ # Automatically leverage output traces to reduce image size
COPY --from=builder /app/.next/static ./.next/static # 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 USER nextjs
EXPOSE 3000 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"] CMD ["node", "server.js"]

View file

@ -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 (
<html lang="en">
<body className={`antialiased`}>{children}</body>
</html>
);
}

View file

@ -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<HTMLInputElement>) => {
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 (
<div className="h-screen w-screen bg-black text-white font-noto flex flex-col items-center">
<Navbar renderHomeLink={true} />
<div className="w-screen h-screen flex flex-col items-center justify-center">
<h2 className="text-5xl font-bold text-white mb-16">rgb-to-hex</h2>
<div className="border-2 border-white p-3 rounded-xl text-center text-white">
<label className="mr-2" htmlFor="red">Red:</label>
<input
type="number"
id="red"
name="red"
min="0"
max="255"
className="w-16 bg-black border-1 border-white text-center"
onInput={checkInput}
/>
<label className="mx-2 text-white" htmlFor="green">Green:</label>
<input
type="number"
id="green"
name="green"
min="0"
max="255"
className="w-16 bg-black border-1 border-white text-center"
onChange={checkInput}
/>
<label className="mx-2" htmlFor="blue">Blue:</label>
<input
type="number"
id="blue"
name="blue"
min="0"
max="255"
className="w-16 bg-black border-1 border-white text-center"
onInput={checkInput}
/>
</div>
<div className={"flex flex-row items-center gap-4 mt-4 mb-16"}>
<Button
content={
loading ? (
<div className="h-10 w-10 border-8 border-blue-100 border-t-blue-500 rounded-full animate-spin" />
) : (
"convert"
)
}
onClick={convertToHex}
/>
<Button
content="clear"
onClick={clearInAndOutput}
/>
</div>
<div className="p-3 rounded-xl text-center">
<output
id="hex"
className="text-blue-400 text-3xl"
>{hex}</output>
</div>
</div>
<Footer />
</div>
);
}