|
1 | 1 | <script> |
| 2 | + import { browser } from '$app/environment'; |
2 | 3 | import '../app.css'; |
3 | 4 | let { data, children } = $props(); |
4 | | - const { botHandle, botActorUrl } = data; |
| 5 | + const { botHandle, botActorUrl, bskyHandle, bskyProfileUrl } = data; |
| 6 | +
|
| 7 | + let instanceModalOpen = $state(false); |
| 8 | + let instanceInput = $state(''); |
| 9 | +
|
| 10 | + $effect(() => { |
| 11 | + if (browser) { |
| 12 | + instanceInput = localStorage.getItem('fedi-instance') || ''; |
| 13 | + } |
| 14 | + }); |
| 15 | +
|
| 16 | + function openOnFediverse(e) { |
| 17 | + e.preventDefault(); |
| 18 | + if (!botActorUrl) return; |
| 19 | + const saved = browser && localStorage.getItem('fedi-instance'); |
| 20 | + if (saved) { |
| 21 | + window.open(`https://${saved}/authorize_interaction?uri=${encodeURIComponent(botActorUrl)}`, '_blank'); |
| 22 | + } else { |
| 23 | + instanceModalOpen = true; |
| 24 | + } |
| 25 | + } |
| 26 | +
|
| 27 | + function submitInstance() { |
| 28 | + const instance = instanceInput.trim().replace(/^https?:\/\//, '').replace(/\/+$/, ''); |
| 29 | + if (!instance) return; |
| 30 | + localStorage.setItem('fedi-instance', instance); |
| 31 | + instanceModalOpen = false; |
| 32 | + window.open(`https://${instance}/authorize_interaction?uri=${encodeURIComponent(botActorUrl)}`, '_blank'); |
| 33 | + } |
5 | 34 | </script> |
6 | 35 |
|
7 | 36 | <svelte:head> |
|
28 | 57 | </main> |
29 | 58 |
|
30 | 59 | <footer class="container"> |
31 | | - {#if botHandle} |
32 | | - <p>Follow on the fediverse: <a href={botActorUrl} rel="me">{botHandle}</a></p> |
33 | | - {/if} |
| 60 | + <div class="follow-links"> |
| 61 | + {#if botHandle} |
| 62 | + <a href={botActorUrl} rel="me" class="follow-link" onclick={openOnFediverse} title="Follow on the fediverse"> |
| 63 | + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 74 79" fill="currentColor"> |
| 64 | + <path d="M73.7 17.7c-1-6.2-6.3-11-12.7-12.3C54.5 4 37.2 2 37.2 2h-.1C27 2 19.5 4 13 5.4 6.6 6.7 1.3 11.5.3 17.7c-.5 3.3-.9 7.1-.9 11 0 3.2.1 6.2.3 9 .7 9.4 5.3 17.7 13.4 21.4 3.8 1.8 8.1 2.7 12.5 3 4.5.3 8.7-.3 12.5-1.7 0 0 0 0 0 0-.3-1.4-.6-2.9-1-4.3-4 1.2-8.3 1.6-12.6 1.3-4.3-.3-8.5-1.7-8.8-6.5-.1-.5-.1-1-.1-1.5 4 1 8.1 1.6 12.3 1.8 2.6.1 5.1 0 7.6-.2 7.1-.7 13.3-2.9 14.1-5.3.6-1.8.8-3.8.8-5.9v-.3c0-6.3 2.4-7.2 2.4-7.2 2.4-1.1 1.3 4.4 1.3 7.2 0 2.4-.3 5.5-1.1 8.5-.5 2-1.4 3.8-2.5 5.4 4.1-1.7 7.5-4.6 9.5-8.5 2.5-5 2.8-10.9 2.8-15.9 0-3.9-.3-7.7-.9-11z"/> |
| 65 | + <path d="M61.2 27.2v22.7h-9V28c0-4.6-1.9-7-5.8-7-4.3 0-6.4 2.8-6.4 8.3v12h-9V29.3c0-5.5-2.2-8.3-6.4-8.3-3.9 0-5.8 2.3-5.8 7v21.9h-9V27.2c0-4.6 1.2-8.3 3.5-11 2.4-2.7 5.6-4.1 9.5-4.1 4.5 0 7.9 1.7 10.2 5.2l2.2 3.7 2.2-3.7c2.2-3.5 5.7-5.2 10.2-5.2 3.9 0 7.1 1.4 9.5 4.1 2.3 2.7 3.5 6.4 3.5 11z"/> |
| 66 | + </svg> |
| 67 | + {botHandle} |
| 68 | + </a> |
| 69 | + {/if} |
| 70 | + {#if bskyHandle} |
| 71 | + <a href={bskyProfileUrl} target="_blank" rel="noopener" class="follow-link" title="Follow on Bluesky"> |
| 72 | + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 568 501" fill="currentColor"> |
| 73 | + <path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.889-129.52 80.986-149.07-65.72 11.185-139.6-7.295-159.875-79.748C10.945 203.659 1 75.291 1 57.946 1-28.906 77.135-1.612 123.121 33.664z"/> |
| 74 | + </svg> |
| 75 | + @{bskyHandle} |
| 76 | + </a> |
| 77 | + {/if} |
| 78 | + </div> |
34 | 79 | <p><a href="https://github.com/rmdes/newsdiff">Source code</a> · <a href="/feed.xml">Atom feed</a></p> |
35 | 80 | </footer> |
36 | 81 |
|
| 82 | +<!-- Fediverse instance picker modal (shared across pages) --> |
| 83 | +{#if instanceModalOpen} |
| 84 | + <div class="modal-backdrop" onclick={() => instanceModalOpen = false}> |
| 85 | + <div class="modal" onclick={(e) => e.stopPropagation()}> |
| 86 | + <h3>Your Mastodon instance</h3> |
| 87 | + <p>Enter your instance to follow the bot from your account.</p> |
| 88 | + <form onsubmit={(e) => { e.preventDefault(); submitInstance(); }}> |
| 89 | + <div class="instance-input"> |
| 90 | + <span class="instance-prefix">https://</span> |
| 91 | + <input |
| 92 | + type="text" |
| 93 | + bind:value={instanceInput} |
| 94 | + placeholder="mastodon.social" |
| 95 | + autofocus |
| 96 | + /> |
| 97 | + </div> |
| 98 | + <div class="modal-actions"> |
| 99 | + <button type="button" class="btn-cancel" onclick={() => instanceModalOpen = false}>Cancel</button> |
| 100 | + <button type="submit" class="btn-go">Follow on fediverse</button> |
| 101 | + </div> |
| 102 | + </form> |
| 103 | + </div> |
| 104 | + </div> |
| 105 | +{/if} |
| 106 | + |
37 | 107 | <style> |
38 | 108 | header { border-bottom: 1px solid var(--color-border); margin-bottom: 2rem; } |
39 | 109 | .header-inner { display: flex; justify-content: space-between; align-items: center; padding-top: 1rem; padding-bottom: 1rem; } |
|
43 | 113 | .nav-links { display: flex; gap: 1.25rem; } |
44 | 114 | .nav-links a { color: var(--color-muted); text-decoration: none; font-size: 1rem; } |
45 | 115 | .nav-links a:hover { color: var(--color-primary); } |
| 116 | +
|
46 | 117 | footer { margin-top: 3rem; padding: 1.5rem 0; border-top: 1px solid var(--color-border); font-size: 0.8rem; color: var(--color-muted); text-align: center; } |
47 | 118 | footer a { color: var(--color-primary); text-decoration: none; } |
48 | 119 | footer a:hover { text-decoration: underline; } |
| 120 | +
|
| 121 | + .follow-links { display: flex; justify-content: center; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 0.75rem; } |
| 122 | + .follow-link { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--color-text); text-decoration: none; } |
| 123 | + .follow-link:hover { color: var(--color-primary); } |
| 124 | + .follow-link svg { opacity: 0.7; } |
| 125 | + .follow-link:hover svg { opacity: 1; } |
| 126 | +
|
| 127 | + /* Instance picker modal */ |
| 128 | + .modal-backdrop { |
| 129 | + position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 100; |
| 130 | + display: flex; align-items: center; justify-content: center; |
| 131 | + } |
| 132 | + .modal { |
| 133 | + background: white; border-radius: 0.5rem; padding: 1.5rem; max-width: 420px; width: 90%; |
| 134 | + box-shadow: 0 8px 32px rgba(0,0,0,0.2); |
| 135 | + } |
| 136 | + .modal h3 { margin: 0 0 0.5rem; font-size: 1.1rem; } |
| 137 | + .modal p { font-size: 0.85rem; color: var(--color-muted); margin-bottom: 1rem; } |
| 138 | + .instance-input { |
| 139 | + display: flex; align-items: center; border: 1px solid var(--color-border); border-radius: 0.25rem; |
| 140 | + overflow: hidden; margin-bottom: 1rem; |
| 141 | + } |
| 142 | + .instance-prefix { padding: 0.5rem; background: #f5f5f5; color: var(--color-muted); font-size: 0.85rem; border-right: 1px solid var(--color-border); } |
| 143 | + .instance-input input { flex: 1; border: none; padding: 0.5rem; font-size: 0.9rem; outline: none; } |
| 144 | + .modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; } |
| 145 | + .btn-cancel { padding: 0.4rem 0.75rem; border: 1px solid var(--color-border); border-radius: 0.25rem; background: white; cursor: pointer; font-size: 0.85rem; } |
| 146 | + .btn-go { padding: 0.4rem 0.75rem; border: none; border-radius: 0.25rem; background: var(--color-primary); color: white; cursor: pointer; font-size: 0.85rem; } |
| 147 | + .btn-go:hover { background: #1d4ed8; } |
49 | 148 | </style> |
0 commit comments