Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption location-sharing privacy self-hosted federated

Compare changes

Choose any two refs to compare.

Changed files
+215 -171
app
+3 -1
app/src/home-page/home.html
··· 14 <div> 15 <!-- somehow the "+" emoji does not display in the code for me, but it's temporary anyways --> 16 <button class="icon-btn" @click="addFriend()">➕</button> 17 - <button class="icon-btn" @click="openSettings()">⚙️</button> 18 </div> 19 </header> 20
··· 14 <div> 15 <!-- somehow the "+" emoji does not display in the code for me, but it's temporary anyways --> 16 <button class="icon-btn" @click="addFriend()">➕</button> 17 + <button class="icon-btn" @click="goto('settings')"> 18 + ⚙️ 19 + </button> 20 </div> 21 </header> 22
+5
app/src/home-page/home.ts
··· 39 isAdmin() { 40 return Store.get("is_admin"); 41 }, 42 })); 43 44 Alpine.start();
··· 39 isAdmin() { 40 return Store.get("is_admin"); 41 }, 42 + 43 + goto(newLocation: string) { 44 + window.location.href = 45 + "/src/" + newLocation + "-page/" + newLocation + ".html"; 46 + }, 47 })); 48 49 Alpine.start();
+116
app/src/settings-page/settings.css
···
··· 1 + body { 2 + font-family: system-ui, sans-serif; 3 + background: #f9fafb; 4 + display: flex; 5 + align-items: center; 6 + justify-content: center; 7 + height: 100vh; 8 + margin: 0; 9 + } 10 + 11 + .card { 12 + max-width: 90%; 13 + background: white; 14 + border: 1px solid #d1d5db; 15 + border-radius: 8px; 16 + padding: 1.5rem; 17 + box-sizing: border-box; 18 + } 19 + 20 + .header { 21 + text-align: center; 22 + margin-bottom: 1.5rem; 23 + } 24 + 25 + .icon-circle { 26 + width: 64px; 27 + height: 64px; 28 + background: #dbeafe; 29 + border-radius: 50%; 30 + display: flex; 31 + align-items: center; 32 + justify-content: center; 33 + margin: 0 auto 1rem; 34 + } 35 + 36 + .icon-circle img { 37 + width: 32px; 38 + height: 32px; 39 + } 40 + 41 + h1 { 42 + font-size: 1.5rem; 43 + } 44 + 45 + p { 46 + font-size: 0.9rem; 47 + color: #6b7280; 48 + } 49 + 50 + .actions { 51 + display: flex; 52 + flex-direction: column; 53 + gap: 1rem; 54 + } 55 + 56 + label { 57 + display: block; 58 + font-size: 0.85rem; 59 + font-weight: 600; 60 + margin-bottom: 0.25rem; 61 + } 62 + 63 + input { 64 + width: 100%; 65 + padding: 0.5rem 0.75rem; 66 + border: 1px solid #d1d5db; 67 + border-radius: 4px; 68 + font-size: 0.95rem; 69 + box-sizing: border-box; 70 + } 71 + 72 + input:focus { 73 + outline: none; 74 + border-color: #2563eb; 75 + } 76 + 77 + button { 78 + width: 100%; 79 + padding: 0.6rem; 80 + font-size: 0.95rem; 81 + border-radius: 4px; 82 + cursor: pointer; 83 + /*transition: background 0.2s ease;*/ 84 + display: flex; 85 + align-items: center; 86 + justify-content: center; 87 + } 88 + 89 + .btn-primary { 90 + background: #2563eb; 91 + color: white; 92 + border: none; 93 + } 94 + 95 + .btn-primary:hover { 96 + background: #1d4ed8; 97 + } 98 + 99 + .btn-qr { 100 + background: white; 101 + gap: 0.5rem; 102 + border: 1px solid #d1d5db; 103 + } 104 + 105 + .btn-qr:hover { 106 + background: #f3f4f6; 107 + } 108 + 109 + .btn-qr img { 110 + width: 16px; 111 + height: 16px; 112 + } 113 + 114 + .hint { 115 + font-size: 0.75rem; 116 + }
+26
app/src/settings-page/settings.html
···
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <script type="module" src="./settings.ts"></script> 6 + <link rel="stylesheet" href="./settings.css" /> 7 + </head> 8 + 9 + <body> 10 + <div class="card"> 11 + <!-- x-data connects this element to the settingsPageState Alpine component, enabling its data (serverAddress and signupKey) and functions (signup and scanQR) to work within it :) --> 12 + <!-- TODO: make this a form instead? --> 13 + <div class="actions" x-data="settingsPageState"> 14 + <h3>Settings</h3> 15 + 16 + <button class="btn-secondary" @click="goto('home')"> 17 + Back to Home 18 + </button> 19 + 20 + <button class="btn-secondary" @click="resetStore()"> 21 + Signout 22 + </button> 23 + </div> 24 + </div> 25 + </body> 26 + </html>
+16
app/src/settings-page/settings.ts
···
··· 1 + import Alpine from "alpinejs"; 2 + import { Store } from "../utils/store.ts"; 3 + import { goto } from "../utils/tools.ts"; 4 + 5 + Alpine.data("settingsPageState", () => ({ 6 + resetStore() { 7 + Store.reset(); 8 + alert("Store reset"); 9 + goto("signup"); 10 + }, 11 + goto(newLocation: string) { 12 + goto(newLocation); 13 + }, 14 + })); 15 + 16 + Alpine.start();
-116
app/src/settings-page/signup.css
··· 1 - body { 2 - font-family: system-ui, sans-serif; 3 - background: #f9fafb; 4 - display: flex; 5 - align-items: center; 6 - justify-content: center; 7 - height: 100vh; 8 - margin: 0; 9 - } 10 - 11 - .card { 12 - max-width: 90%; 13 - background: white; 14 - border: 1px solid #d1d5db; 15 - border-radius: 8px; 16 - padding: 1.5rem; 17 - box-sizing: border-box; 18 - } 19 - 20 - .header { 21 - text-align: center; 22 - margin-bottom: 1.5rem; 23 - } 24 - 25 - .icon-circle { 26 - width: 64px; 27 - height: 64px; 28 - background: #dbeafe; 29 - border-radius: 50%; 30 - display: flex; 31 - align-items: center; 32 - justify-content: center; 33 - margin: 0 auto 1rem; 34 - } 35 - 36 - .icon-circle img { 37 - width: 32px; 38 - height: 32px; 39 - } 40 - 41 - h1 { 42 - font-size: 1.5rem; 43 - } 44 - 45 - p { 46 - font-size: 0.9rem; 47 - color: #6b7280; 48 - } 49 - 50 - .actions { 51 - display: flex; 52 - flex-direction: column; 53 - gap: 1rem; 54 - } 55 - 56 - label { 57 - display: block; 58 - font-size: 0.85rem; 59 - font-weight: 600; 60 - margin-bottom: 0.25rem; 61 - } 62 - 63 - input { 64 - width: 100%; 65 - padding: 0.5rem 0.75rem; 66 - border: 1px solid #d1d5db; 67 - border-radius: 4px; 68 - font-size: 0.95rem; 69 - box-sizing: border-box; 70 - } 71 - 72 - input:focus { 73 - outline: none; 74 - border-color: #2563eb; 75 - } 76 - 77 - button { 78 - width: 100%; 79 - padding: 0.6rem; 80 - font-size: 0.95rem; 81 - border-radius: 4px; 82 - cursor: pointer; 83 - /*transition: background 0.2s ease;*/ 84 - display: flex; 85 - align-items: center; 86 - justify-content: center; 87 - } 88 - 89 - .btn-primary { 90 - background: #2563eb; 91 - color: white; 92 - border: none; 93 - } 94 - 95 - .btn-primary:hover { 96 - background: #1d4ed8; 97 - } 98 - 99 - .btn-qr { 100 - background: white; 101 - gap: 0.5rem; 102 - border: 1px solid #d1d5db; 103 - } 104 - 105 - .btn-qr:hover { 106 - background: #f3f4f6; 107 - } 108 - 109 - .btn-qr img { 110 - width: 16px; 111 - height: 16px; 112 - } 113 - 114 - .hint { 115 - font-size: 0.75rem; 116 - }
···
-42
app/src/settings-page/signup.html
··· 1 - <!doctype html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <script type="module" src="./signup.ts"></script> 6 - <link rel="stylesheet" href="./signup.css" /> 7 - </head> 8 - 9 - <body> 10 - <div class="card"> 11 - <div class="header"> 12 - <div class="icon-circle"> 13 - <img src="/src/assets/pin.svg" alt="Pin Icon" /> 14 - </div> 15 - <h1>PrivacyPin</h1> 16 - <p>Connect with a server to start sharing</p> 17 - </div> 18 - 19 - <!-- x-data connects this element to the signupPageState Alpine component, enabling its data (serverAddress and signupKey) and functions (signup and scanQR) to work within it :) --> 20 - <!-- TODO: make this a form instead? --> 21 - <div class="actions" x-data="signupPageState"> 22 - <div> 23 - <label for="server">Server Address</label> 24 - <input id="server" type="url" placeholder="https://your-server.com" x-model="serverAddress" required /> 25 - </div> 26 - 27 - <div> 28 - <label for="key">Signup Key</label> 29 - <input id="key" type="password" placeholder="Enter your signup key" x-model="signupKey" required /> 30 - </div> 31 - 32 - <p class="hint">Scan a QR code to automatically fill both server address and signup key</p> 33 - <button type="button" x-bind:disabled="isDoingStuff" class="btn-qr" @click="await scanQR()"> 34 - <img src="/src/assets/qr.svg" alt="QR Icon" /> 35 - Scan QR Code 36 - </button> 37 - 38 - <button class="btn-primary" x-bind:disabled="isDoingStuff" @click="await signup()"><span x-show="isDoingStuff">Connecting...</span> <span x-show="!isDoingStuff">Connect</span></button> 39 - </div> 40 - </div> 41 - </body> 42 - </html>
···
-11
app/src/settings-page/signup.ts
··· 1 - import Alpine from "alpinejs"; 2 - import { createAccount } from "../utils/api.ts"; 3 - import { Store } from "../utils/store.ts"; 4 - 5 - Alpine.data("signupPageState", () => ({ 6 - resetStore() { 7 - Store.reset(); 8 - }, 9 - })); 10 - 11 - Alpine.start();
···
+1 -1
app/src/signup-page/signup.ts
··· 23 async scanQR() { 24 this.isDoingStuff = true; 25 await new Promise((resolve) => setTimeout(resolve, 1000)); 26 - this.serverAddress = "dummy server address"; 27 this.signupKey = "dummy signup key"; 28 this.isDoingStuff = false; 29 },
··· 23 async scanQR() { 24 this.isDoingStuff = true; 25 await new Promise((resolve) => setTimeout(resolve, 1000)); 26 + this.serverAddress = "http://127.0.0.1:3000"; 27 this.signupKey = "dummy signup key"; 28 this.isDoingStuff = false; 29 },
+48
app/src/utils/tools.ts
···
··· 1 + export function goto(newLocation: string) { 2 + window.location.href = 3 + "/src/" + newLocation + "-page/" + newLocation + ".html"; 4 + } 5 + 6 + /* 7 + 8 + Use this type of function to toggle dark mode. It CAN be modified to your needs. copy the function, and fix the end comment(be sure to put this in the alpine section) 9 + 10 + toggleDarkMode() { 11 + /* 12 + This toggles darkmode for 'body' in the css file | use for only document types 13 + document.body.classList.toggle("dark-theme"); 14 + 15 + this toggles darkmode for '.app' in the css file | use if it isn't a document type 16 + toggleStyle("app", "dark-theme"); 17 + 18 + * / 19 + 20 + document.body.classList.toggle("dark-theme"); 21 + toggleStyle("header", "dark-theme"); 22 + toggleStyle([".app", ".friend-card", ".content"], "dark-theme"); 23 + }, 24 + */ 25 + 26 + export function toggleStyle(classNames: string | string[], newClass: string) { 27 + if (typeof classNames === "string") { 28 + for ( 29 + let i = 0; 30 + i < document.getElementsByClassName(classNames).length; 31 + i++ 32 + ) { 33 + document.getElementsByClassName(classNames)[i].classList.toggle(newClass); 34 + } 35 + } else { 36 + for (let i = 0; i < classNames.length; i++) { 37 + for ( 38 + let j = 0; 39 + j < document.getElementsByClassName(classNames[i]).length; 40 + j++ 41 + ) { 42 + document 43 + .getElementsByClassName(classNames[i]) 44 + [j].classList.toggle(newClass); 45 + } 46 + } 47 + } 48 + }