+43
-46
src/lib/components/layout/footer/Main.svelte
+43
-46
src/lib/components/layout/footer/Main.svelte
···
1
<script lang="ts">
2
import { onMount } from "svelte";
3
-
import { env } from "$env/dynamic/public";
4
-
import TidClock from "./TidClock.svelte";
5
6
export let profile: any;
7
-
export const posts: any = undefined;
8
9
onMount(() => {
10
const copyrightYearElement = document.getElementById("copyright-year");
···
12
copyrightYearElement.textContent = new Date().getFullYear().toString();
13
}
14
});
0
0
0
0
15
</script>
16
17
-
<footer class="text-center py-8 text-primary text-sm opacity-60">
18
<div class="max-w-2xl mx-auto px-4">
19
-
<!-- Main attribution line -->
20
-
<div class="mb-4">
21
<span>© <span id="copyright-year"></span></span>
22
-
<span class="mx-2">•</span>
23
{#if profile?.handle}
0
24
<a
25
href="https://bsky.app/profile/{profile.did}"
26
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
27
>
28
@{profile.handle}
29
</a>
30
-
{:else}
31
-
<span>{profile?.displayName || profile?.did}</span>
32
{/if}
33
<span class="mx-2">•</span>
34
-
<span>powered by
35
-
<a
36
-
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
37
-
href="https://atproto.com/guides/glossary#at-protocol"
38
-
>
39
-
atproto
40
-
</a>
41
-
</span>
42
</div>
43
44
-
<!-- Project info -->
45
-
<div class="mb-4 text-xs opacity-75 leading-relaxed">
46
-
<div class="mb-2">
47
-
Linkat Directory made by
48
-
<a
49
-
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
50
-
href="https://bsky.app/profile/did:plc:ofrbh253gwicbkc5nktqepol"
51
-
>
52
-
ewan
53
-
</a>
54
-
</div>
55
-
<div>
56
-
<a
57
-
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
58
-
href="https://github.com/ewanc26/linkat-directory"
59
-
>
60
-
Open source
61
-
</a>
62
-
and free to use under AGPL-3.0. Not affiliated with
63
-
<a
64
-
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
65
-
href="https://linkat.blue"
66
-
>
67
-
Linkat
68
-
</a>
0
0
69
</div>
70
-
</div>
71
-
72
-
<!-- Clock -->
73
-
<TidClock />
74
</div>
75
</footer>
···
1
<script lang="ts">
2
import { onMount } from "svelte";
0
0
3
4
export let profile: any;
5
+
let showDetails = false;
6
7
onMount(() => {
8
const copyrightYearElement = document.getElementById("copyright-year");
···
10
copyrightYearElement.textContent = new Date().getFullYear().toString();
11
}
12
});
13
+
14
+
function toggleDetails() {
15
+
showDetails = !showDetails;
16
+
}
17
</script>
18
19
+
<footer class="text-center py-6 text-primary text-sm opacity-60">
20
<div class="max-w-2xl mx-auto px-4">
21
+
<!-- Main footer line -->
22
+
<div class="mb-3">
23
<span>© <span id="copyright-year"></span></span>
0
24
{#if profile?.handle}
25
+
<span class="mx-2">•</span>
26
<a
27
href="https://bsky.app/profile/{profile.did}"
28
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
29
>
30
@{profile.handle}
31
</a>
0
0
32
{/if}
33
<span class="mx-2">•</span>
34
+
<button
35
+
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors underline bg-none border-none cursor-pointer text-sm"
36
+
on:click={toggleDetails}
37
+
>
38
+
{showDetails ? 'Hide details' : 'About'}
39
+
</button>
0
0
40
</div>
41
42
+
<!-- Collapsible details -->
43
+
{#if showDetails}
44
+
<div class="text-xs opacity-75 leading-relaxed space-y-2 transition-all duration-200">
45
+
<div>
46
+
Linkat Directory made by
47
+
<a
48
+
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
49
+
href="https://bsky.app/profile/did:plc:ofrbh253gwicbkc5nktqepol"
50
+
>
51
+
ewan
52
+
</a>
53
+
</div>
54
+
<div>
55
+
<a
56
+
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
57
+
href="https://github.com/ewanc26/linkat-directory"
58
+
>
59
+
Open source
60
+
</a>
61
+
and free to use under AGPL-3.0. Not affiliated with
62
+
<a
63
+
class="text-[var(--link-color)] hover:text-[var(--link-hover-color)] transition-colors"
64
+
href="https://linkat.blue"
65
+
>
66
+
Linkat
67
+
</a>
68
+
</div>
69
</div>
70
+
{/if}
0
0
0
71
</div>
72
</footer>
-96
src/lib/components/layout/footer/TidClock.svelte
-96
src/lib/components/layout/footer/TidClock.svelte
···
1
-
<script lang="ts">
2
-
import { onMount, onDestroy } from "svelte";
3
-
4
-
let currentTID = '';
5
-
let interval: NodeJS.Timeout;
6
-
let isRunning = true;
7
-
8
-
// Base32-sortable character set for TID encoding
9
-
const BASE32_SORTABLE = '234567abcdefghijklmnopqrstuvwxyz';
10
-
11
-
/**
12
-
* Generate a random 10-bit clock identifier
13
-
*/
14
-
function generateClockId(): number {
15
-
return Math.floor(Math.random() * 1024); // 2^10 = 1024
16
-
}
17
-
18
-
/**
19
-
* Convert a number to base32-sortable encoding
20
-
*/
21
-
function toBase32Sortable(num: bigint): string {
22
-
if (num === 0n) {
23
-
return '2222222222222';
24
-
}
25
-
26
-
let result = '';
27
-
while (num > 0n) {
28
-
result = BASE32_SORTABLE[Number(num % 32n)] + result;
29
-
num = num / 32n;
30
-
}
31
-
32
-
// Pad to 13 characters for consistent TID length
33
-
return result.padStart(13, '2');
34
-
}
35
-
36
-
/**
37
-
* Generate a TID for the current timestamp
38
-
*/
39
-
function generateTID(): string {
40
-
// Get current timestamp in microseconds since UNIX epoch
41
-
const nowMs = Date.now();
42
-
const nowMicroseconds = BigInt(nowMs * 1000); // Convert to microseconds
43
-
44
-
// Generate random clock identifier (10 bits)
45
-
const clockId = generateClockId();
46
-
47
-
// Combine timestamp (53 bits) and clock identifier (10 bits)
48
-
// The top bit is always 0, so we have 63 bits in total
49
-
const tidBigInt = (nowMicroseconds << 10n) | BigInt(clockId);
50
-
51
-
return toBase32Sortable(tidBigInt);
52
-
}
53
-
54
-
/**
55
-
* Update the TID display value
56
-
*/
57
-
function updateTID() {
58
-
if (isRunning) {
59
-
currentTID = generateTID();
60
-
}
61
-
}
62
-
63
-
/**
64
-
* Copy the TID to the clipboard
65
-
*/
66
-
async function copyTID() {
67
-
try {
68
-
await navigator.clipboard.writeText(currentTID);
69
-
console.log('TID copied to clipboard:', currentTID);
70
-
} catch (err) {
71
-
console.error('Failed to copy TID:', err);
72
-
}
73
-
}
74
-
75
-
onMount(() => {
76
-
// Generate initial TID
77
-
updateTID();
78
-
79
-
// Update every 100ms for a smooth display
80
-
interval = setInterval(updateTID, 100);
81
-
});
82
-
83
-
onDestroy(() => {
84
-
if (interval) {
85
-
clearInterval(interval);
86
-
}
87
-
});
88
-
</script>
89
-
90
-
<button
91
-
class="inline-block bg-none border-none text-[var(--link-color)] font-mono text-xs cursor-pointer px-0.5 py-0 rounded-md transition-all duration-200 ease-in-out hover:text-[var(--link-hover-color)] hover:bg-[var(--button-bg)]"
92
-
on:click={copyTID}
93
-
title="Click to copy TID"
94
-
>
95
-
{currentTID}
96
-
</button>
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0