Add Rails backend: accounts via magic link, server documents, sharing
The app becomes a Rails 8 application; the Vue frontend moves to app/frontend and is served through vite_rails. - Without an account documents stay in the browser (IndexedDB). Sign-in is by magic link or the 6-digit code from the same mail; afterwards local documents can be uploaded into the account. - Server documents (/d/<id>) with versioned saves and a single-editor lock per browser tab; others see them read-only and follow by polling. - Sharing via link (works without an account) and via e-mail invitation, roles viewer/editor; the owner manages links and people. - Store gains a read-only mode that blocks all content changes. - Solid Queue, Cache and Cable share the one PostgreSQL database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<h1>Link nicht mehr gültig</h1>
|
||||
<p>Dieser Link wurde schon verwendet oder ist abgelaufen. Fordere in numolus einfach einen neuen an.</p>
|
||||
<a class="btn" href="/">Zu numolus</a>
|
||||
@@ -0,0 +1,11 @@
|
||||
<% if @link.invitation? %>
|
||||
<h1>Einladung annehmen</h1>
|
||||
<p><strong><%= @link.invited_by.name %></strong> hat dich zu „<%= @link.document.title %>“ eingeladen. Du wirst dabei als <%= @link.email %> angemeldet.</p>
|
||||
<% else %>
|
||||
<h1>Bei numolus anmelden</h1>
|
||||
<p>Du meldest dich als <strong><%= @link.email %></strong> an.</p>
|
||||
<% end %>
|
||||
<%= form_with url: verify_auth_path, method: :post do %>
|
||||
<%= hidden_field_tag :token, @token %>
|
||||
<button type="submit"><%= @link.invitation? ? "Annehmen und öffnen" : "Anmelden" %></button>
|
||||
<% end %>
|
||||
@@ -0,0 +1,4 @@
|
||||
<p>Hallo,</p>
|
||||
<p><strong><%= @inviter %></strong> hat das Dokument „<%= @title %>“ mit dir geteilt – du kannst es <%= @role %>.</p>
|
||||
<p><a href="<%= @url %>">Dokument öffnen</a></p>
|
||||
<p style="color:#6e6e73">Der Link ist 7 Tage gültig und meldet dich gleichzeitig bei numolus an.</p>
|
||||
@@ -0,0 +1,7 @@
|
||||
Hallo,
|
||||
|
||||
<%= @inviter %> hat das Dokument „<%= @title %>“ mit dir geteilt – du kannst es <%= @role %>.
|
||||
|
||||
<%= @url %>
|
||||
|
||||
Der Link ist 7 Tage gültig und meldet dich gleichzeitig bei numolus an.
|
||||
@@ -0,0 +1,5 @@
|
||||
<p>Hallo,</p>
|
||||
<p>mit diesem Link meldest du dich bei numolus an (gültig 15 Minuten, einmal verwendbar):</p>
|
||||
<p><a href="<%= @url %>">Bei numolus anmelden</a></p>
|
||||
<p>Oder gib diesen Code in numolus ein: <strong style="font-size:18px;letter-spacing:2px"><%= @code %></strong></p>
|
||||
<p style="color:#6e6e73">Falls du das nicht angefordert hast, kannst du diese Mail ignorieren.</p>
|
||||
@@ -0,0 +1,9 @@
|
||||
Hallo,
|
||||
|
||||
mit diesem Link meldest du dich bei numolus an (gültig 15 Minuten, einmal verwendbar):
|
||||
|
||||
<%= @url %>
|
||||
|
||||
Oder gib diesen Code in numolus ein: <%= @code %>
|
||||
|
||||
Falls du das nicht angefordert hast, kannst du diese Mail ignorieren.
|
||||
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Anmelden – numolus</title>
|
||||
<%= csrf_meta_tags %>
|
||||
<style>
|
||||
:root { --bg: #f5f5f7; --surface: #fff; --text: #1d1d1f; --dim: #6e6e73; --accent: #0a84ff; --border: #d2d2d7; }
|
||||
@media (prefers-color-scheme: dark) { :root { --bg: #1c1c1e; --surface: #2c2c2e; --text: #f5f5f7; --dim: #a1a1a6; --border: #3a3a3c; } }
|
||||
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--text);
|
||||
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 16px; box-sizing: border-box; }
|
||||
.card { width: min(400px, 100%); background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 28px; box-sizing: border-box; }
|
||||
h1 { font-size: 20px; margin: 0 0 8px; }
|
||||
p { margin: 0 0 18px; color: var(--dim); }
|
||||
button, .btn { display: inline-block; border: 0; border-radius: 9px; background: var(--accent); color: #fff; font: inherit; font-weight: 600;
|
||||
padding: 10px 18px; cursor: pointer; text-decoration: none; }
|
||||
.brand { display: flex; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 20px; }
|
||||
.brand i { display: inline-block; width: 14px; height: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card">
|
||||
<div class="brand"><i style="background:#FFD400"></i><i style="background:#0057FF"></i><i style="background:#E8384F"></i> numolus</div>
|
||||
<%= yield %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style>
|
||||
/* Email styles need to be inline */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<%= yield %>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Numolus",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"description": "Numolus.",
|
||||
"theme_color": "red",
|
||||
"background_color": "red"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Add a service worker for processing Web Push notifications:
|
||||
//
|
||||
// self.addEventListener("push", async (event) => {
|
||||
// const { title, options } = await event.data.json()
|
||||
// event.waitUntil(self.registration.showNotification(title, options))
|
||||
// })
|
||||
//
|
||||
// self.addEventListener("notificationclick", function(event) {
|
||||
// event.notification.close()
|
||||
// event.waitUntil(
|
||||
// clients.matchAll({ type: "window" }).then((clientList) => {
|
||||
// for (let i = 0; i < clientList.length; i++) {
|
||||
// let client = clientList[i]
|
||||
// let clientPath = (new URL(client.url)).pathname
|
||||
//
|
||||
// if (clientPath == event.notification.data.path && "focus" in client) {
|
||||
// return client.focus()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (clients.openWindow) {
|
||||
// return clients.openWindow(event.notification.data.path)
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
// })
|
||||
@@ -0,0 +1,59 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#f5f5f7" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#1c1c1e" media="(prefers-color-scheme: dark)">
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="Numolus – browserbasierte Tabellenkalkulation nach dem Vorbild von Apple Numbers, mit selbst geschriebener Formel-Engine. wicom, Vaduz (FL)."
|
||||
>
|
||||
<meta name="author" content="Karl-Heinz Wild">
|
||||
<meta name="publisher" content="wicom – Webentwicklung & digitale Lösungen">
|
||||
<meta name="copyright" content="© wicom, Birkenweg 6, FL-9490 Vaduz – https://wicom.li">
|
||||
<meta name="generator" content="Vue 3 + Vite + Rails">
|
||||
<%= csrf_meta_tags %>
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="contact" content="office@wicom.li">
|
||||
<link rel="author" href="https://wicom.li">
|
||||
<link rel="publisher" href="https://wicom.li">
|
||||
<link rel="canonical" href="https://wicom.li/">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="wicom – Webentwicklung & digitale Lösungen">
|
||||
<meta property="og:locale" content="de_LI">
|
||||
<meta property="og:title" content="Numolus · Tabellenkalkulation">
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Tabellenkalkulation à la Apple Numbers mit eigener Formel-Engine – von wicom, Vaduz (FL)."
|
||||
>
|
||||
<meta property="og:url" content="https://wicom.li/">
|
||||
<meta property="og:image" content="https://wicom.li/numolus-logo.svg">
|
||||
|
||||
<!-- Twitter / X Card -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="Numolus · Tabellenkalkulation">
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Tabellenkalkulation à la Apple Numbers mit eigener Formel-Engine – wicom (FL)."
|
||||
>
|
||||
<meta name="twitter:image" content="https://wicom.li/numolus-logo.svg">
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='0' y='0' width='20' height='20' fill='%23FFD400'/%3E%3Crect x='6' y='6' width='20' height='20' fill='%230057FF'/%3E%3Crect x='12' y='12' width='20' height='20' fill='%23E8384F'/%3E%3C/svg%3E"
|
||||
>
|
||||
<title>Numolus · Tabellenkalkulation — wicom</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script id="numolus-boot" type="application/json"><%= raw json_escape(@boot.to_json) %></script>
|
||||
<%= vite_client_tag %>
|
||||
<%= vite_typescript_tag "application" %>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user