feat: some styling

This commit is contained in:
Elias Renman
2025-03-24 16:01:13 +01:00
parent 1f65d20d75
commit cc9b3554e6
8 changed files with 119 additions and 86 deletions

View File

@@ -4,6 +4,7 @@
"": {
"name": "web",
"dependencies": {
"@lucide/svelte": "^0.483.0",
"@tailwindcss/vite": "^4.0.14",
"@tanstack/svelte-query": "^5.69.0",
"axios": "^1.8.4",
@@ -84,6 +85,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@lucide/svelte": ["@lucide/svelte@0.483.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-b3SbhMIgVJAj/rPa3go6uplTzaFkJzz91TSPO8I8gc2evtHOA2OgSmPYz0S+yEKFIWqLUZ4gika19ljV+tnmyQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.35.0", "", { "os": "android", "cpu": "arm" }, "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.35.0", "", { "os": "android", "cpu": "arm64" }, "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA=="],

View File

@@ -19,6 +19,7 @@
"vite": "^6.2.0"
},
"dependencies": {
"@lucide/svelte": "^0.483.0",
"@tailwindcss/vite": "^4.0.14",
"@tanstack/svelte-query": "^5.69.0",
"axios": "^1.8.4",

View File

@@ -5,12 +5,12 @@ import { queryClient } from "./client";
export type UrlUpsertDto = {
url: string;
destinationUrl: string;
ttl: Date;
ttl?: Date;
};
export type Url = {
url: string;
destinationUrl: string;
ttl: Date;
ttl?: Date;
ownedBy: string;
createdAt: Date;
updatedAt: Date;

View File

@@ -1,41 +0,0 @@
<script lang="ts">
export let name: keyof typeof icons;
export let width = "1rem";
export let height = "1rem";
export let focusable: string | number | null | undefined = undefined;
let icons = {
logout: {
box: 32,
svg: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15" />
</svg>`,
},
add: {
box: 32,
svg: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>`,
},
edit: {
box: 32,
svg: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>`,
},
delete: {
box: 32,
svg: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>`,
},
} as const;
let displayIcon = icons[name];
</script>
<svg
class={$$props.class}
{focusable}
{width}
{height}
viewBox="0 0 {displayIcon.box} {displayIcon.box}">{@html displayIcon.svg}</svg
>

View File

@@ -8,29 +8,33 @@
<Navbar />
<div class="h-screen flex items-center justify-center flex-col">
<table>
<thead>
<tr>
<th>Shortform</th>
<th>Destination</th>
<th>Time to live</th>
<th>Action</th>
</tr>
</thead>
<div class="max-w-3xl mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">URL Shortener</h1>
<!-- Table -->
<div class="bg-gray-800 text-white rounded-lg shadow-lg overflow-hidden">
{#if $urls.isLoading}
<tr>
<td><p>Loading...</p></td>
</tr>
<p>Loading...</p>
{:else if $urls.isError}
<tr>
<td><p>Error: {$urls.error.message}</p></td>
</tr>
<p>Error: {$urls.error.message}</p>
{:else if $urls.isSuccess}
{#each $urls.data.data as row}
<Row {row} />
{/each}
<table class="w-full text-left">
<thead class="bg-gray-900">
<tr>
<th class="p-3">Shortform</th>
<th class="p-3">Destination</th>
<th class="p-3">Expire date</th>
<th class="p-3">Actions</th>
</tr>
</thead>
<tbody>
{#each $urls.data.data as row}
<Row {row} />
{/each}
</tbody>
</table>
{/if}
</table>
</div>
<Create />
</div>

View File

@@ -1,24 +1,79 @@
<script lang="ts">
import { mutateUrl, type UrlUpsertDto } from "@/api/url";
import Icon from "@/common/Icon.svelte";
import { ArrowDown } from "@lucide/svelte";
const mutation = mutateUrl();
const handleSubmit = (e: SubmitEvent) => {
e.preventDefault();
const target = e.target as HTMLFormElement;
const data = new FormData(target);
const data = new FormData(e.target as HTMLFormElement);
const formObject = Object.fromEntries(
data.entries(),
) as unknown as UrlUpsertDto;
console.log("Form Data:", formObject);
$mutation.mutate(formObject);
const formObject = Object.fromEntries(data.entries()) as unknown as {
url: string;
destinationUrl: string;
ttl: string;
};
let parsedTtl;
if (formObject.ttl !== "-1") {
parsedTtl = new Date(
new Date().getTime() + parseFloat(formObject.ttl) * 1000,
);
}
$mutation.mutate(
{
...formObject,
ttl: parsedTtl,
},
{
onSuccess: () => {
target.reset();
},
},
);
};
</script>
<form method="POST" on:submit={handleSubmit}>
<input name="url" type="text" class="border-b-1" />
<input name="destinationUrl" type="url" class="border-b-1" />
<button type="submit"><Icon name="add" /></button>
<div class="flex mt-4 p-4 bg-gray-800 rounded-lg shadow-lg gap-4 flex-col">
<div class="flex flex-row gap-2 justify-center items-center">
<p>{window.location.origin}/</p>
<input
class="p-2 bg-gray-900 text-white rounded-lg"
name="url"
type="text"
placeholder="Shortform"
/>
</div>
<div class="flex justify-center">
<ArrowDown />
</div>
<div class="flex flex-row gap-2 items-end">
<input
class="w-full p-2 bg-gray-900 text-white rounded-lg"
name="destinationUrl"
type="url"
placeholder="Destination URL"
/>
<div>
<label for="ttl" class="block text-sm">Expiry</label>
<select
name="ttl"
class="bg-gray-900 text-white rounded-lg p-2 w-full mt-1"
>
<option value="-1" selected>Never</option>
<option value="1800">30 minutes</option>
<option value="3600">1 hour</option>
<option value="86400">1 day</option>
<option value="604800">1 week</option>
<option value="2592000">1 month</option>
</select>
</div>
</div>
<button
class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg shadow hover:bg-blue-500 transition cursor-pointer"
>
Shorten!
</button>
</div>
</form>

View File

@@ -1,13 +1,12 @@
<script lang="ts">
import Icon from "@common/Icon.svelte";
import { decodeJwt } from "./jwt";
import { LogOut } from "@lucide/svelte";
const payload = decodeJwt();
</script>
<div class="flex flexbox justify-between">
<p>{payload?.name}</p>
<a href="/logout">
<Icon name="logout" class="h-5 w-5" />
<LogOut class="h-5 w-5" />
</a>
</div>

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import { mutateDeleteUrl, type Url } from "@/api/url";
import Icon from "@/common/icon.svelte";
import { Edit, Trash } from "@lucide/svelte";
let { row }: { row: Url } = $props();
const mutation = mutateDeleteUrl();
function verifyDelete() {
if (confirm("Are you sure you want to delete this redirect?")) {
@@ -11,16 +12,27 @@
}
</script>
<tr class="border-b-1">
<td><a href={"/" + row.url} aria-label="Redirect url">{row.url}</a></td>
<td
<tr class="border-t border-gray-700 hover:bg-gray-700 transition">
<td class="p-3 font-mono"
><a href={"/" + row.url} aria-label="Redirect url">{row.url}</a></td
>
<td class="p-3 truncate"
><a href={row.destinationUrl} aria-label="Destination url"
>{row.destinationUrl}</a
></td
>
<td></td>
<td class="flex flex-row justify-around"
><Icon name="edit" />
<button onclick={verifyDelete}><Icon name="delete" /></button>
<td class="p-3 truncate"
>{row.ttl ? new Date(row.ttl).toLocaleTimeString() : "Never"}</td
>
<td class="p-3 flex gap-3">
<button class="text-blue-600 hover:text-blue-500 cursor-pointer transition">
<Edit size={18} />
</button>
<button
onclick={verifyDelete}
class="text-red-400 hover:text-red-300 cursor-pointer transition"
>
<Trash size={18} />
</button>
</td>
</tr>