feat: ui remake with tailwind

This commit is contained in:
C4illin
2024-09-09 17:38:03 +02:00
parent ed59cd7aa4
commit 22f823c535
16 changed files with 296 additions and 344 deletions

3
.gitignore vendored
View File

@@ -47,4 +47,5 @@ package-lock.json
/db
/data
/Bruno
/tsconfig.tsbuildinfo
/tsconfig.tsbuildinfo
/src/public/style.css

View File

@@ -1,63 +0,0 @@
FROM oven/bun:1-debian as base
WORKDIR /app
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# FROM base AS install-libjxl-tools
# download
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
# FROM base AS prerelease
# COPY --from=install /temp/dev/node_modules node_modules
# COPY . .
# # [optional] tests & build
# ENV NODE_ENV=production
# RUN bun test
# RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
LABEL maintainer="Emrik Östling (C4illin)"
LABEL description="ConvertX: self-hosted online file converter supporting 700+ file formats."
LABEL repo="https://github.com/C4illin/ConvertX"
# install additional dependencies
RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
&& apt-get install -y \
pandoc \
texlive-latex-recommended \
texlive-fonts-recommended \
texlive-latex-extra \
ffmpeg \
graphicsmagick \
ghostscript \
libvips-tools
# # libjxl is not available in the official debian repositories
# RUN wget https://github.com/libjxl/libjxl/releases/download/v0.10.2/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -O /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz \
# && mkdir -p /tmp/libjxl \
# && tar -xvf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -C /tmp/libjxl \
# && dpkg -i /tmp/libjxl/libjxl_0.10.2_amd64.deb /tmp/libjxl/jxl_0.10.2_amd64.deb \
# && rm -rf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz /tmp/libjxl
COPY --from=install /temp/prod/node_modules node_modules
# COPY --from=prerelease /app/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json .
COPY . .
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]

View File

@@ -22,14 +22,14 @@ RUN cargo install resvg
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
# FROM base AS prerelease
# COPY --from=install /temp/dev/node_modules node_modules
# COPY . .
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# # [optional] tests & build
# ENV NODE_ENV=production
ENV NODE_ENV=production
# RUN bun test
# RUN bun run build
RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
@@ -56,6 +56,7 @@ RUN apk --no-cache add \
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
COPY --from=prerelease /app/src/public/style.css /app/src/public/
# COPY --from=prerelease /app/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json .
COPY . .

View File

@@ -10,9 +10,14 @@
"attributePosition": "auto"
},
"files": {
"ignore": ["**/node_modules/**", "**/pico.lime.min.css"]
"ignore": [
"**/node_modules/**",
"**/pico.lime.min.css"
]
},
"organizeImports": {
"enabled": true
},
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
@@ -25,7 +30,11 @@
"useLiteralKeys": "error",
"useOptionalChain": "error"
},
"correctness": { "noPrecisionLoss": "error", "noUnusedVariables": "off" },
"correctness": {
"noPrecisionLoss": "error",
"noUnusedVariables": "off",
"useJsxKeyInIterable": "off"
},
"style": {
"noInferrableTypes": "error",
"noNamespace": "error",
@@ -45,6 +54,9 @@
"noUnsafeDeclarationMerging": "error",
"useAwait": "error",
"useNamespaceKeyword": "error"
},
"nursery": {
"useSortedClasses": "error"
}
}
},
@@ -60,4 +72,4 @@
"attributePosition": "auto"
}
}
}
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -5,7 +5,7 @@
"dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx",
"format": "biome format --write ./src",
"css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat",
"build": "postcss ./src/main.css -o ./src/public/style.css",
"lint": "run-p 'lint:*'",
"lint:tsc": "tsc --noEmit",
"lint:knip": "knip",
@@ -36,7 +36,8 @@
"@types/node": "^22.5.4",
"@typescript-eslint/eslint-plugin": "^8.4.0",
"@typescript-eslint/parser": "^8.4.0",
"cpy-cli": "^5.0.0",
"autoprefixer": "^10.4.20",
"cssnano": "^7.0.6",
"eslint": "^9.9.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0",
@@ -47,7 +48,12 @@
"eslint-plugin-simple-import-sort": "^12.1.1",
"knip": "^5.29.2",
"npm-run-all2": "^6.2.2",
"postcss": "^8.4.47",
"postcss-cli": "^11.0.0",
"postcss-lightningcss": "^1.0.1",
"prettier": "^3.3.3",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.12",
"typescript": "^5.5.4",
"typescript-eslint": "^8.4.0"
},

9
postcss.config.cjs Normal file
View File

@@ -0,0 +1,9 @@
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
// eslint-disable-next-line no-undef
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
}
}

View File

@@ -7,7 +7,6 @@ export const BaseHtml = ({
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title safe>{title}</title>
<link rel="stylesheet" href="/pico.lime.min.css" />
<link rel="stylesheet" href="/style.css" />
<link
rel="apple-touch-icon"
@@ -28,6 +27,6 @@ export const BaseHtml = ({
/>
<link rel="manifest" href="/site.webmanifest" />
</head>
<body>{children}</body>
<body class="w-full bg-gray-900 text-gray-200">{children}</body>
</html>
);

View File

@@ -5,44 +5,53 @@ export const Header = ({
let rightNav: JSX.Element;
if (loggedIn) {
rightNav = (
<ul>
<ul class="flex gap-4 ">
<li>
<a href="/history">History</a>
<a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline"
href="/history">
History
</a>
</li>
<li>
<a href="/logoff">Logout</a>
<a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline"
href="/logoff">
Logout
</a>
</li>
</ul>
);
} else {
rightNav = (
<ul>
<ul class="flex gap-4">
<li>
<a href="/login">Login</a>
<a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline"
href="/login">
Login
</a>
</li>
{accountRegistration && (
{accountRegistration ? (
<li>
<a href="/register">Register</a>
<a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline"
href="/register">
Register
</a>
</li>
)}
) : null}
</ul>
);
}
return (
<header class="container">
<nav>
<header class="w-full p-4">
<nav class="mx-auto flex max-w-4xl justify-between rounded bg-gray-900 p-4">
<ul>
<li>
<strong>
<a
href="/"
style={{
textDecoration: "none",
color: "inherit",
}}>
ConvertX
</a>
<a href="/">ConvertX</a>
</strong>
</li>
</ul>

17
src/helpers/tailwind.ts Normal file
View File

@@ -0,0 +1,17 @@
import tw from "tailwindcss";
import postcss from "postcss";
export const generateTailwind = async () => {
const result = await Bun.file("./src/main.css")
.text()
.then((sourceText) => {
const config = "./tailwind.config.js";
return postcss([tw(config)]).process(sourceText, {
from: "./src/main.css",
to: "./public/style.css",
});
});
return result;
};

View File

@@ -138,36 +138,45 @@ const app = new Elysia({
return (
<BaseHtml title="ConvertX | Setup">
<main class="container">
<h1>Welcome to ConvertX</h1>
<article>
<header>Create your account</header>
<form method="post" action="/register">
<fieldset>
<label>
Email/Username
<main class="w-full mx-auto max-w-4xl px-4">
<h1 class="text-3xl my-8">Welcome to ConvertX!</h1>
<article class="article p-0">
<header class="w-full bg-gray-800 p-4">Create your account</header>
<form method="post" action="/register" class="p-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded bg-gray-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded bg-gray-800 p-3"
placeholder="Password"
autocomplete="current-password"
required
/>
</label>
</fieldset>
<input type="submit" value="Create account" />
<input type="submit" value="Create account" class="btn-primary" />
</form>
<footer>
<footer class="p-4">
Report any issues on{" "}
<a href="https://github.com/C4illin/ConvertX">GitHub</a>.
<a
class="underline text-lime-500 hover:text-lime-400"
href="https://github.com/C4illin/ConvertX">
GitHub
</a>
.
</footer>
</article>
</main>
@@ -183,32 +192,38 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Register">
<>
<Header accountRegistration={ACCOUNT_REGISTRATION} />
<main class="container">
<article>
<form method="post">
<fieldset>
<label>
<main class="w-full px-4">
<article class="article">
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded bg-gray-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded bg-gray-800 p-3"
placeholder="Password"
autocomplete="new-password"
autocomplete="current-password"
required
/>
</label>
</fieldset>
<input type="submit" value="Register" />
<input
type="submit"
value="Register"
class="btn-primary w-full"
/>
</form>
</article>
</main>
@@ -299,25 +314,27 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Login">
<>
<Header accountRegistration={ACCOUNT_REGISTRATION} />
<main class="container">
<article>
<form method="post">
<fieldset>
<label>
<main class="w-full px-4">
<article class="article">
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded bg-gray-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded bg-gray-800 p-3"
placeholder="Password"
autocomplete="current-password"
required
@@ -325,12 +342,16 @@ const app = new Elysia({
</label>
</fieldset>
<div role="group">
{ACCOUNT_REGISTRATION && (
{ACCOUNT_REGISTRATION ? (
<a href="/register" role="button" class="secondary">
Register an account
</a>
)}
<input type="submit" value="Login" />
) : null}
<input
type="submit"
value="Login"
class="btn-primary w-full"
/>
</div>
</form>
</article>
@@ -452,7 +473,7 @@ const app = new Elysia({
value: accessToken,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 60 * 60 * 24 * 1,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
}
@@ -491,90 +512,63 @@ const app = new Elysia({
<BaseHtml>
<>
<Header loggedIn />
<main class="container">
<article>
<h1>Convert</h1>
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
<table id="file-list" class="striped" />
<main class="w-full px-4">
<article class="article">
<h1 class="mb-4 text-xl">Convert</h1>
<div class="max-h-[50vh] overflow-y-auto mb-4 scrollbar-thin">
<table
id="file-list"
class="table-auto w-full bg-gray-900 [&_td]:p-4 rounded [&_tr]:border-b [&_tr]:border-gray-800 [&_tr]:rounded"
/>
</div>
<div
id="dropzone"
class="relative flex h-48 w-full items-center justify-center rounded border border-gray-700 border-dashed transition-all hover:border-gray-600 [&.dragover]:border-4 [&.dragover]:border-gray-500">
<span>
<b>Choose a file</b> or drag it here
</span>
<input
type="file"
name="file"
multiple
class="absolute inset-0 size-full cursor-pointer opacity-0"
/>
</div>
<input type="file" name="file" multiple />
{/* <label for="convert_from">Convert from</label> */}
{/* <select name="convert_from" aria-label="Convert from" required>
<option selected disabled value="">
Convert from
</option>
{getPossibleInputs().map((input) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option>{input}</option>
))}
</select> */}
</article>
<form
method="post"
action="/convert"
style={{ position: "relative" }}>
class="relative w-full mx-auto max-w-4xl mb-[35vh]">
<input type="hidden" name="file_names" id="file_names" />
<article>
<article class="article w-full">
<input
type="search"
name="convert_to_search"
placeholder="Search for conversions"
autocomplete="off"
class="w-full rounded bg-gray-800 p-4"
/>
<div class="select_container">
<article
class="convert_to_popup"
hidden
style={{
flexDirection: "column",
display: "flex",
zIndex: 2,
position: "absolute",
maxHeight: "50vh",
width: "90vw",
overflowY: "scroll",
margin: "0px",
overflowX: "hidden",
}}>
<div class="select_container relative">
<article class="convert_to_popup flex-col absolute z-[2] max-h-[50vh] h-[30vh] w-full overflow-y-auto m-0 overflow-x-hidden hidden bg-gray-800 sm:h-[30vh] rounded">
{Object.entries(getAllTargets()).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<article
class="convert_to_group"
data-converter={converter}
style={{
borderColor: "gray",
padding: "2px",
}}
>
<header
style={{ fontSize: "20px", fontWeight: "bold" }}
safe>
class="convert_to_group border-gray-700 border-b p-4 w-full"
data-converter={converter}>
<header class="text-xl font-bold w-full mb-2" safe>
{converter}
</header>
<ul
class="convert_to_target"
style={{
display: "flex",
flexDirection: "row",
gap: "5px",
flexWrap: "wrap",
}}>
<ul class="convert_to_target flex flex-row gap-1 flex-wrap">
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class="target"
class="target p-1 text-base bg-gray-700 rounded hover:bg-gray-600"
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
style={{ fontSize: "15px", padding: "5px" }}
type="button"
safe
>
safe>
{target}
</button>
))}
@@ -595,10 +589,8 @@ const app = new Elysia({
</option>
{Object.entries(getAllTargets()).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<optgroup label={converter}>
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option value={`${target},${converter}`} safe>
{target}
</option>
@@ -609,7 +601,7 @@ const app = new Elysia({
</select>
</div>
</article>
<input type="submit" value="Convert" />
<input class="btn-primary w-full" type="submit" value="Convert" />
</form>
</main>
<script src="script.js" defer />
@@ -622,56 +614,26 @@ const app = new Elysia({
({ body }) => {
return (
<>
<article
class="convert_to_popup"
hidden
style={{
flexDirection: "column",
display: "flex",
zIndex: 2,
position: "absolute",
maxHeight: "50vh",
width: "90vw",
overflowY: "scroll",
margin: "0px",
overflowX: "hidden",
}}>
<article class="convert_to_popup flex-col absolute z-[2] max-h-[50vh] h-[50vh] w-full overflow-y-auto m-0 overflow-x-hidden hidden bg-gray-800 sm:h-[30vh] rounded">
{Object.entries(getPossibleTargets(body.fileType)).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<article
class="convert_to_group"
data-converter={converter}
style={{
borderColor: "gray",
padding: "2px",
}}
>
<header style={{ fontSize: "20px", fontWeight: "bold" }} safe>
class="convert_to_group border-gray-700 border-b p-4 w-full"
data-converter={converter}>
<header class="text-xl font-bold w-full mb-2" safe>
{converter}
</header>
<ul
class="convert_to_target"
style={{
display: "flex",
flexDirection: "row",
gap: "5px",
flexWrap: "wrap",
}}>
<ul class="convert_to_target flex flex-row gap-1 flex-wrap">
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class="target"
class="target p-1 text-base bg-gray-700 rounded hover:bg-gray-600"
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
style={{ fontSize: "15px", padding: "5px" }}
type="button"
safe
>
safe>
{target}
</button>
))}
@@ -687,10 +649,8 @@ const app = new Elysia({
</option>
{Object.entries(getPossibleTargets(body.fileType)).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<optgroup label={converter}>
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option value={`${target},${converter}`} safe>
{target}
</option>
@@ -736,7 +696,7 @@ const app = new Elysia({
await Bun.write(`${userUploadsDir}${file.name}`, file);
}
} else {
// biome-ignore lint/complexity/useLiteralKeys: weird error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/dot-notation
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
}
}
@@ -913,29 +873,32 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Results">
<>
<Header loggedIn />
<main class="container">
<article>
<h1>Results</h1>
<table>
<main class="w-full px-4">
<article class="article">
<h1 class="text-xl mb-4">Results</h1>
<table class="table-auto w-full bg-gray-900 [&_td]:p-4 rounded [&_tr]:border-b [&_tr]:border-gray-800 [&_tr]:rounded text-left">
<thead>
<tr>
<th>Time</th>
<th>Files</th>
<th>Files Done</th>
<th>Status</th>
<th>View</th>
<th class="px-4 py-2">Time</th>
<th class="px-4 py-2">Files</th>
<th class="px-4 py-2">Files Done</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">View</th>
</tr>
</thead>
<tbody>
{userJobs.map((job) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr>
<td safe>{job.date_created}</td>
<td>{job.num_files}</td>
<td>{job.finished_files}</td>
<td safe>{job.status}</td>
<td>
<a href={`/results/${job.id}`}>View</a>
<a
class="underline text-lime-500 hover:text-lime-400"
href={`/results/${job.id}`}>
View
</a>
</td>
</tr>
))}
@@ -987,14 +950,14 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Result">
<>
<Header loggedIn />
<main class="container">
<article>
<div class="grid">
<h1>Results</h1>
<main class="w-full px-4">
<article class="article">
<div class="flex items-center justify-between mb-4">
<h1 class="text-xl">Results</h1>
<div>
<button
type="button"
style={{ width: "10rem", float: "right" }}
class="w-40 float-right btn-primary"
onclick="downloadAll()"
{...(files.length !== job.num_files
? { disabled: true, "aria-busy": "true" }
@@ -1005,30 +968,35 @@ const app = new Elysia({
</button>
</div>
</div>
<progress max={job.num_files} value={files.length} />
<table>
<progress
max={job.num_files}
value={files.length}
class="w-full rounded-full mb-4 h-2 border-0 inline-block appearance-none overflow-hidden bg-none bg-gray-700 text-lime-500 accent-lime-500 [&::-moz-progress-bar]:bg-gray-700 [&::-webkit-progress-value]:[ background:none] [&::-webkit-progress-value]:rounded-full [&[value]::-webkit-progress-value]:bg-lime-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"
/>
<table class="table-auto w-full bg-gray-900 [&_td]:p-4 rounded [&_tr]:border-b [&_tr]:border-gray-800 [&_tr]:rounded text-left">
<thead>
<tr>
<th>Converted File Name</th>
<th>Status</th>
<th>View</th>
<th>Download</th>
<th class="px-4 py-2">Converted File Name</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">View</th>
<th class="px-4 py-2">Download</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr>
<td safe>{file.output_file_name}</td>
<td safe>{file.status}</td>
<td>
<a
class="underline text-lime-500 hover:text-lime-400"
href={`/download/${outputPath}${file.output_file_name}`}>
View
</a>
</td>
<td>
<a
class="underline text-lime-500 hover:text-lime-400"
href={`/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}>
Download
@@ -1083,13 +1051,13 @@ const app = new Elysia({
.all(params.jobId);
return (
<article>
<div class="grid">
<h1>Results</h1>
<article class="article">
<div class="flex items-center justify-between mb-4">
<h1 class="text-xl">Results</h1>
<div>
<button
type="button"
style={{ width: "10rem", float: "right" }}
class="w-40 float-right btn-primary"
onclick="downloadAll()"
{...(files.length !== job.num_files
? { disabled: true, "aria-busy": "true" }
@@ -1100,29 +1068,35 @@ const app = new Elysia({
</button>
</div>
</div>
<progress max={job.num_files} value={files.length} />
<table>
<progress
max={job.num_files}
value={files.length}
class="w-full rounded-full mb-4 h-2 border-0 inline-block appearance-none overflow-hidden bg-none bg-gray-700 text-lime-500 accent-lime-500 [&::-moz-progress-bar]:bg-gray-700 [&::-webkit-progress-value]:[ background:none] [&::-webkit-progress-value]:rounded-full [&[value]::-webkit-progress-value]:bg-lime-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"
/>
<table class="table-auto w-full bg-gray-900 [&_td]:p-4 rounded [&_tr]:border-b [&_tr]:border-gray-800 [&_tr]:rounded text-left">
<thead>
<tr>
<th>Converted File Name</th>
<th>Status</th>
<th>View</th>
<th>Download</th>
<th class="px-4 py-2">Converted File Name</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">View</th>
<th class="px-4 py-2">Download</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr>
<td safe>{file.output_file_name}</td>
<td safe>{file.status}</td>
<td>
<a href={`/download/${outputPath}${file.output_file_name}`}>
<a
class="underline text-lime-500 hover:text-lime-400"
href={`/download/${outputPath}${file.output_file_name}`}>
View
</a>
</td>
<td>
<a
class="underline text-lime-500 hover:text-lime-400"
href={`/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}>
Download
@@ -1178,15 +1152,15 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Converters">
<>
<Header loggedIn />
<main class="container">
<article>
<h1>Converters</h1>
<table>
<main class="w-full px-4">
<article class="article">
<h1 class="mb-4 text-xl">Converters</h1>
<table class="table-auto w-full bg-gray-900 [&_td]:p-4 rounded [&_tr]:border-b [&_tr]:border-gray-800 [&_tr]:rounded text-left [&_ul]:list-inside [&_ul]:list-disc">
<thead>
<tr>
<th>Converter</th>
<th>From (Count)</th>
<th>To (Count)</th>
<th class="mx-4 my-2">Converter</th>
<th class="mx-4 my-2">From (Count)</th>
<th class="mx-4 my-2">To (Count)</th>
</tr>
</thead>
<tbody>
@@ -1194,14 +1168,12 @@ const app = new Elysia({
([converter, targets]) => {
const inputs = getAllInputs(converter);
return (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr>
<td safe>{converter}</td>
<td>
Count: {inputs.length}
<ul>
{inputs.map((input) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<li safe>{input}</li>
))}
</ul>
@@ -1210,7 +1182,6 @@ const app = new Elysia({
Count: {targets.length}
<ul>
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<li safe>{target}</li>
))}
</ul>
@@ -1258,8 +1229,23 @@ const app = new Elysia({
.onError(({ error }) => {
// log.error(` ${request.method} ${request.url}`, code, error);
console.error(error);
})
.listen(3000);
});
if (process.env.NODE_ENV !== "production") {
await import("./helpers/tailwind").then(
async ({ generateTailwind }) => {
const result = await generateTailwind()
app.get("/style.css", ({ set }) => {
set.headers["content-type"] = "text/css";
return result;
});
},
);
}
app.listen(3000);
console.log(
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`,

12
src/main.css Normal file
View File

@@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.article {
@apply p-4 mb-4 bg-gray-800/40 w-full mx-auto max-w-4xl rounded;
}
.btn-primary {
@apply bg-lime-500 text-black rounded p-4 hover:bg-lime-400 cursor-pointer;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,17 @@
// Select the file input element
const fileInput = document.querySelector('input[type="file"]');
const dropZone = document.getElementById("dropzone");
const fileNames = [];
let fileType;
dropZone.addEventListener("dragover", (e) => {
dropZone.classList.add("dragover");
});
dropZone.addEventListener("dragleave", (e) => {
dropZone.classList.remove("dragover");
});
const selectContainer = document.querySelector("form .select_container");
const updateSearchBar = () => {
@@ -20,16 +29,20 @@ const updateSearchBar = () => {
for (const target of targets) {
if (target.dataset.target.includes(search)) {
matchingTargetsFound++;
target.hidden = false;
target.classList.remove("hidden");
target.classList.add("flex");
} else {
target.hidden = true;
target.classList.add("hidden");
target.classList.remove("flex");
}
}
if (matchingTargetsFound === 0) {
groupElement.hidden = true;
groupElement.classList.add("hidden");
groupElement.classList.remove("flex");
} else {
groupElement.hidden = false;
groupElement.classList.remove("hidden");
groupElement.classList.add("flex");
}
}
};
@@ -59,15 +72,18 @@ const updateSearchBar = () => {
// Keep the popup open even when clicking on a target button
// for a split second to allow the click to go through
if (e?.relatedTarget?.classList?.contains("target")) {
convertToPopup.hidden = true;
convertToPopup.classList.add("hidden");
convertToPopup.classList.remove("flex");
return;
}
convertToPopup.hidden = true;
convertToPopup.classList.add("hidden");
convertToPopup.classList.remove("flex");
});
convertToInput.addEventListener("focus", () => {
convertToPopup.hidden = false;
convertToPopup.classList.remove("hidden");
convertToPopup.classList.add("flex");
});
};
@@ -94,6 +110,7 @@ fileInput.addEventListener("change", (e) => {
if (!fileType) {
fileType = file.name.split(".").pop();
console.log("fileType", fileType);
fileInput.setAttribute("accept", `.${fileType}`);
setTitle();

View File

@@ -1,59 +0,0 @@
div.icon {
height: 100px;
width: 100px;
}
button[type="submit"] {
width: 50%;
}
div.center {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
@media (max-width: 99999999999px) {
.convert_to_popup {
width: 50vw !important;
height: 50vh;
}
}
@media (max-width: 850px) {
.convert_to_popup {
width: 60vw !important;
height: 60vh;
}
}
@media (max-width: 575px) {
.convert_to_popup {
width: 80vw !important;
height: 75vh;
}
}
@media (max-height: 1000px) {
.convert_to_popup {
height: 40vh;
}
}
@media (max-height: 650px) {
.convert_to_popup {
height: 30vh;
}
}
@media (max-height: 500px) {
.convert_to_popup {
height: 25vh;
}
}
@media (max-height: 400px) {
.convert_to_popup {
height: 15vh;
}
}

9
tailwind.config.js Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
// eslint-disable-next-line no-undef
module.exports = {
content: ["./src/**/*.{html,js,tsx}"],
theme: {
extend: {},
},
plugins: [import('tailwind-scrollbar')],
}