Jelajahi Sumber

feat: ui remake with tailwind

C4illin 10 bulan lalu
induk
melakukan
22f823c535
16 mengubah file dengan 296 tambahan dan 343 penghapusan
  1. 2 1
      .gitignore
  2. 0 63
      Debian.Dockerfile
  3. 6 5
      Dockerfile
  4. 16 4
      biome.json
  5. TEMPAT SAMPAH
      bun.lockb
  6. 8 2
      package.json
  7. 9 0
      postcss.config.cjs
  8. 1 2
      src/components/base.tsx
  9. 27 18
      src/components/header.tsx
  10. 17 0
      src/helpers/tailwind.ts
  11. 165 179
      src/index.tsx
  12. 12 0
      src/main.css
  13. 0 3
      src/public/pico.lime.min.css
  14. 24 7
      src/public/script.js
  15. 0 59
      src/public/style.css
  16. 9 0
      tailwind.config.js

+ 2 - 1
.gitignore

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

+ 0 - 63
Debian.Dockerfile

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

+ 6 - 5
Dockerfile

@@ -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 . .

+ 16 - 4
biome.json

@@ -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"
     }
   }
-}
+}

TEMPAT SAMPAH
bun.lockb


+ 8 - 2
package.json

@@ -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 - 0
postcss.config.cjs

@@ -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: {} } : {})
+  }
+}

+ 1 - 2
src/components/base.tsx

@@ -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>
 );

+ 27 - 18
src/components/header.tsx

@@ -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 - 0
src/helpers/tailwind.ts

@@ -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;
+};

+ 165 - 179
src/index.tsx

@@ -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 - 0
src/main.css

@@ -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 ditekan karena terlalu besar
+ 0 - 3
src/public/pico.lime.min.css


+ 24 - 7
src/public/script.js

@@ -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();
 

+ 0 - 59
src/public/style.css

@@ -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 - 0
tailwind.config.js

@@ -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')],
+}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini