mirror of
https://github.com/eliasrenman/url-shortener.git
synced 2026-03-16 20:16:06 +01:00
feat: some styling
This commit is contained in:
@@ -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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user