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