jsx working

This commit is contained in:
C4illin 2024-05-19 00:07:56 +02:00
parent 0f0bc6c4e5
commit a68046ecd6
19 changed files with 814 additions and 458 deletions

13
src/components/base.tsx Normal file
View file

@ -0,0 +1,13 @@
export const BaseHtml = ({ children, title = "ConvertX" }) => (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<link rel="stylesheet" href="/pico.lime.min.css" />
<link rel="stylesheet" href="/style.css" />
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>{children}</body>
</html>
);

49
src/components/header.tsx Normal file
View file

@ -0,0 +1,49 @@
export const Header = ({ loggedIn }: { loggedIn?: boolean }) => {
let rightNav: JSX.Element;
if (loggedIn) {
rightNav = (
<ul>
<li>
<a href="/results">History</a>
</li>
<li>
<a href="/logout">Logout</a>
</li>
</ul>
);
} else {
rightNav = (
<ul>
<li>
<a href="/login">Login</a>
</li>
<li>
<a href="/register">Register</a>
</li>
</ul>
);
}
return (
<header class="container-fluid">
<nav>
<ul>
<li>
<strong>
<a
href="/"
style={{
textDecoration: "none",
color: "inherit",
}}
>
ConvertX
</a>
</strong>
</li>
</ul>
{rightNav}
</nav>
</header>
);
};

39
src/converters/main.ts Normal file
View file

@ -0,0 +1,39 @@
import { properties, convert } from "./sharp";
export async function mainConverter(
inputFilePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
) {
// Check if the fileType and convertTo are supported by the sharp converter
if (properties.from.includes(fileType) && properties.to.includes(convertTo)) {
// Use the sharp converter
try {
await convert(inputFilePath, fileType, convertTo, targetPath, options);
console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully.`,
);
} catch (error) {
console.error(
`Failed to convert ${inputFilePath} from ${fileType} to ${convertTo}.`,
error,
);
}
} else {
console.log(
`The sharp converter does not support converting from ${fileType} to ${convertTo}.`,
);
}
}
export function possibleConversions(fileType: string) {
// Check if the fileType is supported by the sharp converter
if (properties.from.includes(fileType)) {
return properties.to;
}
return [];
}

37
src/converters/sharp.ts Normal file
View file

@ -0,0 +1,37 @@
import sharp from "sharp";
// declare possible conversions
export const properties = {
from: ["jpeg", "png", "webp", "gif", "avif", "tiff", "svg"],
to: ["jpeg", "png", "webp", "gif", "avif", "tiff"],
options: {
svg: {
scale: {
description: "Scale the image up or down",
type: "number",
default: 1,
},
}
}
}
export async function convert(filePath: string, fileType: string, convertTo: string, targetPath: string, options?: any) {
if (fileType === "svg") {
const scale = options.scale || 1;
const metadata = await sharp(filePath).metadata();
if (!metadata || !metadata.width || !metadata.height) {
throw new Error("Could not get metadata from image");
}
const newWidth = Math.round(metadata.width * scale)
const newHeight = Math.round(metadata.height * scale)
return await sharp(filePath)
.resize(newWidth, newHeight)
.toFormat(convertTo)
.toFile(targetPath);
}
return await sharp(filePath).toFormat(convertTo).toFile(targetPath);
}

View file

@ -0,0 +1,12 @@
export const normalizeFiletype = (filetype: string): string => {
const lowercaseFiletype = filetype.toLowerCase();
switch (lowercaseFiletype) {
case "jpg":
return "jpeg";
case "htm":
return "html";
default:
return lowercaseFiletype;
}
}

View file

@ -1,247 +0,0 @@
import { Elysia, t } from "elysia";
import { staticPlugin } from "@elysiajs/static";
import { html } from "@elysiajs/html";
import { Database } from "bun:sqlite";
import cookie from "@elysiajs/cookie";
import { unlink } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { jwt } from "@elysiajs/jwt";
const db = new Database("./mydb.sqlite");
const uploadsDir = "./uploads/";
// init db
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
job_id TEXT NOT NULL,
date_created TEXT NOT NULL
);`);
const app = new Elysia()
.use(cookie())
.use(
jwt({
name: "jwt",
schema: t.Object({
id: t.String(),
}),
secret: "secret",
exp: "7d",
}),
)
.use(html())
.use(
staticPlugin({
assets: "src/public/",
prefix: "/",
}),
)
.get("/register", async () => {
return Bun.file("src/pages/register.html");
})
.post(
"/register",
async function handler({ body, set, jwt, cookie: { auth } }) {
const existingUser = await db
.query("SELECT * FROM users WHERE email = ?")
.get(body.email);
if (existingUser) {
set.status = 400;
return {
message: "Email already in use.",
};
}
const savedPassword = await Bun.password.hash(body.password);
db.run(
"INSERT INTO users (email, password) VALUES (?, ?)",
body.email,
savedPassword,
);
const user = await db
.query("SELECT * FROM users WHERE email = ?")
.get(body.email);
const accessToken = await jwt.sign({
id: String(user.id),
});
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: "strict",
});
// redirect to home
set.status = 302;
set.headers = {
Location: "/",
};
},
)
.get("/login", async () => {
return Bun.file("src/pages/login.html");
})
.post("/login", async function handler({ body, set, jwt, cookie: { auth } }) {
const existingUser = await db
.query("SELECT * FROM users WHERE email = ?")
.get(body.email);
if (!existingUser) {
set.status = 403;
return {
message: "Invalid credentials.",
};
}
const validPassword = await Bun.password.verify(
body.password,
existingUser.password,
);
if (!validPassword) {
set.status = 403;
return {
message: "Invalid credentials.",
};
}
const accessToken = await jwt.sign({
id: String(existingUser.id),
});
// set cookie
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: "strict",
});
// redirect to home
set.status = 302;
set.headers = {
Location: "/",
};
})
.post("/logout", async ({ set, cookie: { auth } }) => {
auth.remove();
set.status = 302;
set.headers = {
Location: "/login",
};
})
.get("/", async ({ jwt, set, cookie: { auth, jobId } }) => {
// validate jwt
const user = await jwt.verify(auth.value);
if (!user) {
// redirect to login
set.status = 302;
set.headers = {
Location: "/login",
};
return;
}
// make sure user exists in db
const existingUser = await db
.query("SELECT * FROM users WHERE id = ?")
.get(user.id);
if (!existingUser) {
// redirect to login and clear cookie
auth.remove();
set.status = 302;
set.headers = {
Location: "/login",
};
return;
}
// create a unique job id
jobId.set({
value: randomUUID(),
httpOnly: true,
secure: true,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
// insert job id into db
db.run(
"INSERT INTO jobs (user_id, job_id, date_created) VALUES (?, ?, ?)",
user.id,
jobId.value,
new Date().toISOString(),
);
return Bun.file("src/pages/index.html");
})
.post("/upload", async ({ body, set, jwt, cookie: { auth, jobId } }) => {
// validate jwt
const user = await jwt.verify(auth.value);
if (!user) {
// redirect to login
set.status = 302;
set.headers = {
Location: "/login",
};
return;
}
// let filesUploaded = [];
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
if (body?.file) {
await Bun.write(`${userUploadsDir}${body.file.name}`, body.file);
// filesUploaded.push(body.file.name);
} else if (body?.files) {
if (Array.isArray(body.files)) {
for (const file of body.files) {
console.log(file);
await Bun.write(`${userUploadsDir}${file.name}`, file);
// filesUploaded.push(file.name);
}
} else {
await Bun.write(`${userUploadsDir}${body.files.name}`, body.files);
// filesUploaded.push(body.files.name);
}
}
})
.post("/delete", async ({ body, set, jwt, cookie: { auth, jobId } }) => {
const user = await jwt.verify(auth.value);
if (!user) {
// redirect to login
set.status = 302;
set.headers = {
Location: "/login",
};
return;
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
await unlink(`${userUploadsDir}${body.filename}`);
})
.post("/convert", async (ctx) => {
console.log(ctx.body);
})
.listen(3000);
console.log(
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`,
);

460
src/index.tsx Normal file
View file

@ -0,0 +1,460 @@
import { randomUUID } from "node:crypto";
import { mkdir, unlink } from "node:fs/promises";
import cookie from "@elysiajs/cookie";
import { html } from "@elysiajs/html";
import { jwt } from "@elysiajs/jwt";
import { staticPlugin } from "@elysiajs/static";
import { Database } from "bun:sqlite";
import { Elysia, t } from "elysia";
import { BaseHtml } from "./components/base";
import { Header } from "./components/header";
import { mainConverter, possibleConversions } from "./converters/main";
import { normalizeFiletype } from "./helpers/normalizeFiletype";
const db = new Database("./db/mydb.sqlite");
const uploadsDir = "./uploads/";
const outputDir = "./output/";
const jobs = {};
// init db
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
job_id TEXT NOT NULL,
date_created TEXT NOT NULL,
status TEXT DEFAULT 'pending'
);`);
const app = new Elysia()
.use(cookie())
.use(html())
.use(
jwt({
name: "jwt",
schema: t.Object({
id: t.String(),
}),
secret: "secret",
exp: "7d",
}),
)
.use(
staticPlugin({
assets: "src/public/",
prefix: "/",
}),
)
.get("/register", () => {
return (
<BaseHtml title="ConvertX | Register">
<Header />
<main class="container-fluid">
<form method="post">
<input type="email" name="email" placeholder="Email" required />
<input
type="password"
name="password"
placeholder="Password"
required
/>
<input type="submit" value="Register" />
</form>
</main>
</BaseHtml>
);
})
.post(
"/register",
async function handler({ body, set, jwt, cookie: { auth } }) {
const existingUser = await db
.query("SELECT * FROM users WHERE email = ?")
.get(body.email);
if (existingUser) {
set.status = 400;
return {
message: "Email already in use.",
};
}
const savedPassword = await Bun.password.hash(body.password);
db.run(
"INSERT INTO users (email, password) VALUES (?, ?)",
body.email,
savedPassword,
);
const user = await db
.query("SELECT * FROM users WHERE email = ?")
.get(body.email);
const accessToken = await jwt.sign({
id: String(user.id),
});
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: "strict",
});
// redirect to home
set.status = 302;
set.headers = {
Location: "/",
};
},
)
.get("/login", () => {
return (
<BaseHtml title="ConvertX | Login">
<Header />
<main class="container-fluid">
<form method="post">
<input type="email" name="email" placeholder="Email" required />
<input
type="password"
name="password"
placeholder="Password"
required
/>
<div role="group">
<a href="/register" role="button" class="secondary">
Register an account
</a>
<input type="submit" value="Login" />
</div>
</form>
</main>
</BaseHtml>
);
})
.post("/login", async function handler({ body, set, jwt, cookie: { auth } }) {
const existingUser = await db
.query("SELECT * FROM users WHERE email = ?")
.get(body.email);
if (!existingUser) {
set.status = 403;
return {
message: "Invalid credentials.",
};
}
const validPassword = await Bun.password.verify(
body.password,
existingUser.password,
);
if (!validPassword) {
set.status = 403;
return {
message: "Invalid credentials.",
};
}
const accessToken = await jwt.sign({
id: String(existingUser.id),
});
// set cookie
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: "strict",
});
// redirect to home
set.status = 302;
set.headers = {
Location: "/",
};
})
.get("/logout", ({ redirect, cookie: { auth } }) => {
if (auth?.value) {
auth.remove();
}
return redirect("/login");
})
.post("/logout", ({ redirect, cookie: { auth } }) => {
if (auth?.value) {
auth.remove();
}
return redirect("/login");
})
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
// validate jwt
const user = await jwt.verify(auth.value);
if (!user) {
return redirect("/login");
}
// make sure user exists in db
const existingUser = await db
.query("SELECT * FROM users WHERE id = ?")
.get(user.id);
if (!existingUser) {
if (auth?.value) {
auth.remove();
}
return redirect("/login");
}
// create a unique job id
jobId.set({
value: randomUUID(),
httpOnly: true,
secure: true,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
// insert job id into db
db.run(
"INSERT INTO jobs (user_id, job_id, date_created) VALUES (?, ?, ?)",
user.id,
jobId.value,
new Date().toISOString(),
);
return (
<BaseHtml>
<Header loggedIn />
<main class="container-fluid">
<article>
<table id="file-list" />
<input type="file" name="file" multiple />
</article>
<form method="post" action="/convert">
<input type="hidden" name="file_names" id="file_names" />
<article>
<select name="convert_to" aria-label="Convert to" required>
<option selected disabled value="">
Convert to
</option>
<option>JPG</option>
<option>PNG</option>
<option>SVG</option>
<option>PDF</option>
<option>DOCX</option>
<option>Yaml</option>
</select>
</article>
<input type="submit" value="Convert" />
</form>
</main>
<script src="script.js" defer />
</BaseHtml>
);
})
.post("/upload", async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
// validate jwt
if (!auth?.value) {
// redirect to login
return redirect("/login");
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect("/login");
}
// let filesUploaded = [];
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
if (body?.file) {
if (Array.isArray(body.file)) {
for (const file of body.file) {
console.log(file);
await Bun.write(`${userUploadsDir}${file.name}`, file);
}
} else {
await Bun.write(`${userUploadsDir}${body.file.name}`, body.file);
}
}
return {
message: "Files uploaded successfully.",
};
})
.post("/delete", async ({ body, set, jwt, cookie: { auth, jobId } }) => {
const user = await jwt.verify(auth.value);
if (!user) {
// redirect to login
set.status = 302;
set.headers = {
Location: "/login",
};
return;
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
await unlink(`${userUploadsDir}${body.filename}`);
})
.post(
"/convert",
async ({ body, set, redirect, jwt, cookie: { auth, jobId } }) => {
const user = await jwt.verify(auth.value);
if (!user) {
// redirect to login
set.status = 302;
set.headers = {
Location: "/login",
};
return;
}
if (!jobId?.value) {
return redirect("/");
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`;
// create the output directory
try {
await mkdir(userOutputDir, { recursive: true });
} catch (error) {
console.error(
`Failed to create the output directory: ${userOutputDir}.`,
error,
);
}
const convertTo = normalizeFiletype(body.convert_to);
const fileNames = JSON.parse(body.file_names);
jobs[jobId.value] = {
fileNames: fileNames,
filesToConvert: fileNames.length,
convertedFiles: 0,
outputFiles: [],
};
for (const fileName of fileNames) {
const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop();
const fileType = normalizeFiletype(fileTypeOrig);
const newFileName = fileName.replace(fileTypeOrig, convertTo);
const targetPath = `${userOutputDir}${newFileName}`;
await mainConverter(filePath, fileType, convertTo, targetPath);
jobs[jobId.value].convertedFiles++;
jobs[jobId.value].outputFiles.push(newFileName);
}
console.log(
"sending to results page...",
`http://${app.server?.hostname}:${app.server?.port}/results/${jobId.value}`,
);
// redirect to results
set.status = 302;
set.headers = {
Location: `/results/${jobId.value}`,
};
},
)
.get("/results", async ({ params, jwt, set, redirect, cookie: { auth } }) => {
if (!auth?.value) {
return redirect("/login");
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect("/login");
}
const userJobs = await db
.query("SELECT * FROM jobs WHERE user_id = ?")
.all(user.id);
return (
<BaseHtml title="ConvertX | Results">
<Header loggedIn />
<main class="container-fluid">
<article>
<h1>Results</h1>
<ul>
{userJobs.map((job) => (
<li>
<a href={`/results/${job.job_id}`}>{job.job_id}</a>
</li>
))}
</ul>
</article>
</main>
</BaseHtml>
);
// list all jobs belonging to the user
})
.get(
"/results/:jobId",
async ({ params, jwt, set, redirect, cookie: { auth } }) => {
if (!auth?.value) {
return redirect("/login");
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect("/login");
}
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND job_id = ?")
.get(user.id, params.jobId);
if (!job) {
set.status = 404;
return {
message: "Job not found.",
};
}
return (
<BaseHtml>
<Header loggedIn />
<main class="container-fluid">
<article>
<h1>Results</h1>
<ul>
{jobs[params.jobId].outputFiles.map((file: string) => (
<li>
<a href={`/output/${user.id}/${params.jobId}/${file}`}>
{file}
</a>
</li>
))}
</ul>
</article>
</main>
</BaseHtml>
);
},
)
.onError(({ code, error, request }) => {
// log.error(` ${request.method} ${request.url}`, code, error);
console.error(error);
})
.listen(3000);
console.log(
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`,
);

View file

@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ConvertX</title>
<link rel="stylesheet" href="pico.lime.min.css">
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<header class="container-fluid">
<nav>
<ul>
<li><a href="/">ConvertX</a></strong></li>
</ul>
<ul>
<li><a href="#">About</a></li>
<li><a href="#">Services</a></li>
<li><button class="secondary">Products</button></li>
</ul>
</nav>
</header>
<main class="container-fluid">
<!-- File upload -->
<article>
<table id="file-list">
</table>
<input type="file" name="file" multiple />
</article>
<!-- <div class="icon">></div> -->
<form method="post"></form>
<article>
<select name="to" aria-label="Convert to" required>
<option selected disabled value="">Convert to</option>
<option>JPG</option>
<option>PNG</option>
<option>SVG</option>
<option>PDF</option>
<option>DOCX</option>
<option>Yaml</option>
</select>
</article>
<input type="submit" value="Convert">
<!-- <button type="submit">Convert</button> -->
<!-- </div> -->
</form>
</main>
<footer></footer>
</body>
</html>

View file

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ConvertX | Login</title>
<link rel="stylesheet" href="pico.lime.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="container-fluid">
<nav>
<ul>
<li><a href="/">ConvertX</a></strong></li>
</ul>
<ul>
<li><a href="#">About</a></li>
<li><a href="#">Services</a></li>
<li><button class="secondary">Products</button></li>
</ul>
</nav>
</header>
<main class="container-fluid">
<form method="post">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<div role="group">
<a href="/register" role="button" class="secondary">Register an account</a>
<input type="submit" value="Login">
</div>
</form>
</main>
</body>
</html>

View file

@ -1,7 +1,7 @@
// Select the file input element
const fileInput = document.querySelector('input[type="file"]');
const fileNames = [];
const filesToUpload = [];
// Add a 'change' event listener to the file input element
fileInput.addEventListener("change", (e) => {
@ -13,17 +13,20 @@ fileInput.addEventListener("change", (e) => {
const fileList = document.querySelector("#file-list");
// Loop through the selected files
for (let i = 0; i < files.length; i++) {
for (const file of files) {
// Create a new table row for each file
const row = document.createElement("tr");
row.innerHTML = `
<td>${files[i].name}</td>
<td>${(files[i].size / 1024 / 1024).toFixed(2)} MB</td>
<td>${file.name}</td>
<td>${(file.size / 1024 / 1024).toFixed(2)} MB</td>
<td><button class="secondary" onclick="deleteRow(this)">x</button></td>
`;
// Append the row to the file-list table
fileList.appendChild(row);
// Append the file to the hidden input
fileNames.push(file.name);
}
uploadFiles(files);
@ -35,6 +38,10 @@ const deleteRow = (target) => {
const row = target.parentElement.parentElement;
row.remove();
// remove from fileNames
const index = fileNames.indexOf(filename);
fileNames.splice(index, 1);
fetch("/delete", {
method: "POST",
body: JSON.stringify({ filename: filename }),
@ -52,8 +59,8 @@ const deleteRow = (target) => {
const uploadFiles = (files) => {
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append("files", files[i], files[i].name);
for (const file of files) {
formData.append("file", file, file.name);
}
fetch("/upload", {
@ -66,3 +73,11 @@ const uploadFiles = (files) => {
})
.catch((err) => console.log(err));
};
const formConvert = document.querySelector("form[action='/convert']");
formConvert.addEventListener("submit", (e) => {
const hiddenInput = document.querySelector("input[name='file_names']");
hiddenInput.value = JSON.stringify(fileNames);
});