feat: add InfoPopup with landing page content on first visit
Shows an informational popup with rich landing page content on first visit, re-openable via a fixed info button (bottom-right corner). Dismissible via X button, overlay click, or Escape key. Uses localStorage to track first-visit state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ce05a51960
commit
77302e1a6e
|
|
@ -5,6 +5,8 @@ import { AuthProvider } from '@/components/AuthProvider'
|
|||
import { PWAInstall } from '@/components/PWAInstall'
|
||||
import { SubdomainSession } from '@/components/SubdomainSession'
|
||||
import { Header } from '@/components/Header'
|
||||
import InfoPopup from "@/components/InfoPopup"
|
||||
import { LANDING_HTML } from "@/components/landing-content"
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
|
|
@ -53,6 +55,7 @@ export default function RootLayout({
|
|||
{children}
|
||||
<PWAInstall />
|
||||
</AuthProvider>
|
||||
<InfoPopup appName="rNotes" appIcon="📝" landingHtml={LANDING_HTML} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,366 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
interface InfoPopupProps {
|
||||
appName: string;
|
||||
appIcon: string;
|
||||
landingHtml: string;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
const CSS = `
|
||||
/* ── CSS Variables (dark theme defaults) ── */
|
||||
.info-popup-scope {
|
||||
--rs-bg-surface: #16213e;
|
||||
--rs-bg-page: #1a1a2e;
|
||||
--rs-bg-hover: rgba(255,255,255,0.04);
|
||||
--rs-primary: #e94560;
|
||||
--rs-accent: #14b8a6;
|
||||
--rs-text-primary: #eee;
|
||||
--rs-text-secondary: #b0b0c0;
|
||||
--rs-text-muted: #6b7280;
|
||||
--rs-border: rgba(255,255,255,0.08);
|
||||
--rs-border-subtle: rgba(255,255,255,0.05);
|
||||
--rs-card-bg: rgba(255,255,255,0.03);
|
||||
--rs-card-border: rgba(255,255,255,0.06);
|
||||
--rs-gradient-brand: linear-gradient(135deg, #14b8a6, #22d3ee);
|
||||
--rs-gradient-cta: linear-gradient(135deg, #14b8a6, #06b6d4);
|
||||
--rs-btn-secondary-bg: rgba(255,255,255,0.06);
|
||||
--rs-border-strong: rgba(255,255,255,0.15);
|
||||
--rs-bg-surface-raised: rgba(255,255,255,0.06);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* ── Info button (fixed, bottom-right) ── */
|
||||
.info-popup-trigger {
|
||||
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
|
||||
width: 44px; height: 44px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(20,184,166,0.15), rgba(79,70,229,0.1));
|
||||
border: 1px solid rgba(20,184,166,0.3);
|
||||
color: #14b8a6; font-size: 1.2rem; font-weight: 700;
|
||||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
|
||||
}
|
||||
.info-popup-trigger:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 28px rgba(0,0,0,0.4);
|
||||
border-color: rgba(20,184,166,0.5);
|
||||
}
|
||||
|
||||
/* ── Backdrop overlay ── */
|
||||
.rapp-info-overlay {
|
||||
position: fixed; inset: 0; z-index: 10000;
|
||||
background: rgba(0,0,0,0.45);
|
||||
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
||||
animation: rapp-overlay-in 0.2s ease-out;
|
||||
}
|
||||
@keyframes rapp-overlay-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
/* ── Panel — centered modal ── */
|
||||
.rapp-info-panel {
|
||||
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
z-index: 10001;
|
||||
width: min(520px, calc(100vw - 32px)); max-height: calc(100vh - 64px);
|
||||
background: var(--rs-bg-surface);
|
||||
border: 1px solid rgba(20,184,166,0.25);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(20,184,166,0.1), 0 0 60px rgba(20,184,166,0.06);
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
animation: rapp-info-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
@keyframes rapp-info-in {
|
||||
from { opacity: 0; transform: translate(-50%, -48%) scale(0.95); }
|
||||
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
}
|
||||
|
||||
/* ── Header with icon + name ── */
|
||||
.rapp-info-panel__header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, rgba(20,184,166,0.12), rgba(79,70,229,0.08));
|
||||
border-bottom: 1px solid rgba(20,184,166,0.18);
|
||||
}
|
||||
.rapp-info-panel__header-left { display: flex; align-items: center; gap: 10px; }
|
||||
.rapp-info-panel__icon { font-size: 1.5rem; line-height: 1; }
|
||||
.rapp-info-panel__title {
|
||||
font-size: 1.05rem; font-weight: 700; letter-spacing: 0.01em;
|
||||
background: linear-gradient(135deg, #14b8a6, #22d3ee);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.rapp-info-panel__close {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 32px; height: 32px;
|
||||
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08);
|
||||
color: var(--rs-text-muted); font-size: 1.2rem; cursor: pointer;
|
||||
border-radius: 8px; line-height: 1;
|
||||
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.rapp-info-panel__close:hover {
|
||||
color: var(--rs-text-primary); background: rgba(255,255,255,0.1);
|
||||
border-color: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
/* ── Body ── */
|
||||
.rapp-info-panel__body {
|
||||
padding: 0; overflow-y: auto; flex: 1;
|
||||
color: var(--rs-text-secondary); font-size: 0.92rem; line-height: 1.65;
|
||||
}
|
||||
.rapp-info-panel__body h1, .rapp-info-panel__body h2, .rapp-info-panel__body h3 {
|
||||
color: var(--rs-text-primary); margin: 0 0 8px;
|
||||
}
|
||||
.rapp-info-panel__body h1 { font-size: 1.3rem; }
|
||||
.rapp-info-panel__body h2 { font-size: 1.1rem; }
|
||||
.rapp-info-panel__body h3 { font-size: 0.95rem; }
|
||||
.rapp-info-panel__body p { margin: 0 0 10px; }
|
||||
.rapp-info-panel__body a { color: var(--rs-primary); }
|
||||
|
||||
/* ── Panel-scoped overrides for rich landing content ── */
|
||||
.rapp-info-panel__body .rl-hero { padding: 1.75rem 1.5rem 1.25rem; text-align: center; }
|
||||
.rapp-info-panel__body .rl-hero .rl-heading { font-size: 1.5rem !important; }
|
||||
.rapp-info-panel__body .rl-heading { font-size: 1.2rem; margin-bottom: 0.5rem; }
|
||||
.rapp-info-panel__body .rl-tagline { font-size: 0.65rem; padding: 0.3rem 0.85rem; margin-bottom: 1rem; }
|
||||
.rapp-info-panel__body .rl-subtitle { font-size: 1rem !important; margin-bottom: 0.75rem; }
|
||||
.rapp-info-panel__body .rl-subtext { font-size: 0.9rem !important; margin-bottom: 1.25rem; line-height: 1.7; }
|
||||
.rapp-info-panel__body .rl-hero .rl-subtext { font-size: 0.92rem !important; }
|
||||
.rapp-info-panel__body .rl-section { padding: 1.5rem 1.25rem; border-top: 1px solid var(--rs-border-subtle); }
|
||||
.rapp-info-panel__body .rl-section--alt { background: rgba(20,184,166,0.03); }
|
||||
.rapp-info-panel__body .rl-container { max-width: 100%; }
|
||||
.rapp-info-panel__body .rl-grid-2,
|
||||
.rapp-info-panel__body .rl-grid-3,
|
||||
.rapp-info-panel__body .rl-grid-4 { grid-template-columns: 1fr 1fr !important; gap: 0.75rem; }
|
||||
.rapp-info-panel__body .rl-card { padding: 1.15rem; border-radius: 0.75rem; }
|
||||
.rapp-info-panel__body .rl-card h3 { font-size: 0.88rem; margin-bottom: 0.35rem; }
|
||||
.rapp-info-panel__body .rl-card p { font-size: 0.82rem; line-height: 1.55; margin-bottom: 0; }
|
||||
.rapp-info-panel__body .rl-icon-box { width: 2.5rem; height: 2.5rem; font-size: 1.25rem; border-radius: 0.6rem; margin-bottom: 0.65rem; }
|
||||
.rapp-info-panel__body .rl-card--center .rl-icon-box { margin: 0 auto 0.65rem; }
|
||||
.rapp-info-panel__body .rl-step__num { width: 2.25rem; height: 2.25rem; font-size: 0.75rem; margin-bottom: 0.5rem; }
|
||||
.rapp-info-panel__body .rl-step h3 { font-size: 0.88rem; }
|
||||
.rapp-info-panel__body .rl-step p { font-size: 0.82rem; }
|
||||
.rapp-info-panel__body .rl-cta-row { margin-top: 1.5rem; gap: 0.625rem; display: flex; flex-wrap: wrap; justify-content: center; }
|
||||
.rapp-info-panel__body .rl-cta-primary {
|
||||
padding: 0.75rem 1.5rem; font-size: 0.92rem; font-weight: 600;
|
||||
border-radius: 10px; text-decoration: none;
|
||||
background: var(--rs-gradient-cta, linear-gradient(135deg, #14b8a6, #06b6d4));
|
||||
color: #fff; box-shadow: 0 4px 16px rgba(20,184,166,0.3);
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.rapp-info-panel__body .rl-cta-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(20,184,166,0.4); }
|
||||
.rapp-info-panel__body .rl-cta-secondary {
|
||||
padding: 0.75rem 1.5rem; font-size: 0.92rem; font-weight: 600;
|
||||
border-radius: 10px; text-decoration: none;
|
||||
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12);
|
||||
color: var(--rs-text-secondary); transition: transform 0.15s, border-color 0.15s, color 0.15s, background 0.15s;
|
||||
}
|
||||
.rapp-info-panel__body .rl-cta-secondary:hover { transform: translateY(-1px); border-color: rgba(20,184,166,0.4); color: var(--rs-text-primary); background: rgba(20,184,166,0.08); }
|
||||
.rapp-info-panel__body a[onclick*="startTour"],
|
||||
.rapp-info-panel__body a[href*="tour"] { display: none; }
|
||||
.rapp-info-panel__body .rl-badge { font-size: 0.65rem; padding: 0.15rem 0.5rem; }
|
||||
.rapp-info-panel__body .rl-integration { padding: 1rem; border-radius: 0.75rem; gap: 0.75rem; }
|
||||
.rapp-info-panel__body .rl-integration h3 { font-size: 0.88rem; }
|
||||
.rapp-info-panel__body .rl-integration p { font-size: 0.82rem; }
|
||||
.rapp-info-panel__body .rl-check-list li { font-size: 0.82rem; padding: 0.3rem 0; }
|
||||
.rapp-info-panel__body .rl-divider { margin: 1rem 0; }
|
||||
.rapp-info-panel__body .rl-divider span { font-size: 0.65rem; }
|
||||
.rapp-info-panel__body .rl-back { padding: 1.25rem 0 1.5rem; }
|
||||
.rapp-info-panel__body .rl-back a { font-size: 0.82rem; }
|
||||
.rapp-info-panel__body::-webkit-scrollbar { width: 5px; }
|
||||
.rapp-info-panel__body::-webkit-scrollbar-track { background: transparent; }
|
||||
.rapp-info-panel__body::-webkit-scrollbar-thumb { background: rgba(20,184,166,0.2); border-radius: 9999px; }
|
||||
.rapp-info-panel__body::-webkit-scrollbar-thumb:hover { background: rgba(20,184,166,0.35); }
|
||||
|
||||
/* ── Rich Landing CSS ── */
|
||||
.rl-section { border-top: 1px solid var(--rs-border-subtle); padding: 4rem 1.5rem; }
|
||||
.rl-section--alt { background: var(--rs-bg-hover); }
|
||||
.rl-container { max-width: 1100px; margin: 0 auto; }
|
||||
.rl-hero { text-align: center; padding: 5rem 1.5rem 3rem; max-width: 820px; margin: 0 auto; }
|
||||
.rl-tagline {
|
||||
display: inline-block; font-size: 0.7rem; font-weight: 700;
|
||||
letter-spacing: 0.12em; text-transform: uppercase;
|
||||
color: var(--rs-accent); background: rgba(20,184,166,0.1);
|
||||
border: 1px solid rgba(20,184,166,0.2);
|
||||
padding: 0.35rem 1rem; border-radius: 9999px; margin-bottom: 1.5rem;
|
||||
}
|
||||
.rl-heading {
|
||||
font-size: 2rem; font-weight: 700; line-height: 1.15;
|
||||
margin-bottom: 0.75rem; letter-spacing: -0.01em;
|
||||
background: var(--rs-gradient-brand);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
|
||||
}
|
||||
.rl-hero .rl-heading { font-size: 2.5rem; }
|
||||
.rl-subtitle { font-size: 1.25rem; font-weight: 500; color: var(--rs-text-primary); margin-bottom: 1rem; }
|
||||
.rl-hero .rl-subtitle { font-size: 1.35rem; }
|
||||
.rl-subtext { font-size: 1.05rem; color: var(--rs-text-secondary); line-height: 1.65; max-width: 640px; margin: 0 auto 2rem; }
|
||||
.rl-hero .rl-subtext { font-size: 1.15rem; }
|
||||
.rl-grid-2 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
|
||||
.rl-grid-3 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
|
||||
.rl-grid-4 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
|
||||
@media (min-width: 640px) {
|
||||
.rl-grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.rl-grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.rl-grid-4 { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (min-width: 1024px) { .rl-grid-4 { grid-template-columns: repeat(4, 1fr); } }
|
||||
.rl-card {
|
||||
background: var(--rs-card-bg); border: 1px solid var(--rs-card-border);
|
||||
border-radius: 1rem; padding: 1.75rem; transition: border-color 0.2s;
|
||||
}
|
||||
.rl-card:hover { border-color: rgba(20,184,166,0.3); }
|
||||
.rl-card h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.5rem; }
|
||||
.rl-card p { font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.6; }
|
||||
.rl-card--center { text-align: center; }
|
||||
.rl-step { display: flex; flex-direction: column; align-items: center; text-align: center; }
|
||||
.rl-step__num {
|
||||
width: 2.5rem; height: 2.5rem; border-radius: 9999px;
|
||||
background: rgba(20,184,166,0.1); color: var(--rs-accent);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.8rem; font-weight: 700; margin-bottom: 0.75rem;
|
||||
}
|
||||
.rl-step h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.25rem; }
|
||||
.rl-step p { font-size: 0.82rem; color: var(--rs-text-secondary); line-height: 1.55; }
|
||||
.rl-cta-row { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem; }
|
||||
.rl-cta-primary {
|
||||
display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
|
||||
background: var(--rs-gradient-cta); color: white; font-size: 0.95rem; font-weight: 600;
|
||||
text-decoration: none; transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.rl-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.3); }
|
||||
.rl-cta-secondary {
|
||||
display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
|
||||
background: var(--rs-btn-secondary-bg); border: 1px solid var(--rs-border);
|
||||
color: var(--rs-text-secondary); font-size: 0.95rem; font-weight: 600;
|
||||
text-decoration: none; transition: transform 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
.rl-cta-secondary:hover { transform: translateY(-2px); border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
|
||||
.rl-check-list { list-style: none; padding: 0; margin: 0; }
|
||||
.rl-check-list li {
|
||||
display: flex; align-items: flex-start; gap: 0.5rem;
|
||||
font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.55; padding: 0.35rem 0;
|
||||
}
|
||||
.rl-check-list li::before { content: "\\2713"; color: var(--rs-accent); font-weight: 700; flex-shrink: 0; margin-top: 0.05em; }
|
||||
.rl-check-list li strong { color: var(--rs-text-primary); font-weight: 600; }
|
||||
.rl-badge {
|
||||
display: inline-block; font-size: 0.65rem; font-weight: 700;
|
||||
color: white; background: var(--rs-accent);
|
||||
padding: 0.15rem 0.5rem; border-radius: 9999px;
|
||||
}
|
||||
.rl-divider { display: flex; align-items: center; gap: 0.75rem; margin: 1.5rem 0; }
|
||||
.rl-divider::before, .rl-divider::after { content: ""; flex: 1; height: 1px; background: var(--rs-border-subtle); }
|
||||
.rl-divider span { font-size: 0.75rem; color: var(--rs-text-muted); white-space: nowrap; }
|
||||
.rl-icon-box {
|
||||
width: 3rem; height: 3rem; border-radius: 0.75rem;
|
||||
background: rgba(20,184,166,0.12); color: var(--rs-accent);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.5rem; margin-bottom: 1rem;
|
||||
}
|
||||
.rl-card--center .rl-icon-box { margin: 0 auto 1rem; }
|
||||
.rl-integration {
|
||||
display: flex; align-items: flex-start; gap: 1rem;
|
||||
background: rgba(20,184,166,0.04); border: 1px solid rgba(20,184,166,0.15);
|
||||
border-radius: 1rem; padding: 1.5rem;
|
||||
}
|
||||
.rl-integration h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.35rem; }
|
||||
.rl-integration p { font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.55; }
|
||||
.rl-back { padding: 2rem 0 3rem; text-align: center; }
|
||||
.rl-back a { font-size: 0.85rem; color: var(--rs-text-muted); text-decoration: none; transition: color 0.2s; }
|
||||
.rl-back a:hover { color: var(--rs-text-primary); }
|
||||
.rl-progress { height: 0.5rem; border-radius: 9999px; background: var(--rs-border-subtle); overflow: hidden; }
|
||||
.rl-progress__fill { height: 100%; border-radius: 9999px; background: var(--rs-accent); }
|
||||
.rl-tier { display: flex; gap: 0.5rem; margin: 1rem 0; }
|
||||
.rl-tier__item { flex: 1; text-align: center; border-radius: 0.5rem; border: 1px solid var(--rs-border-subtle); padding: 0.5rem; font-size: 0.75rem; }
|
||||
.rl-tier__item--active { border-color: rgba(20,184,166,0.4); background: rgba(20,184,166,0.05); color: var(--rs-accent); }
|
||||
.rl-tier__item--active strong { color: var(--rs-accent); }
|
||||
.rl-zoom-bar { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.rl-zoom-bar__row { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.rl-zoom-bar__label { font-size: 0.7rem; color: var(--rs-text-muted); width: 1.2rem; text-align: right; font-family: monospace; }
|
||||
.rl-zoom-bar__bar { height: 1.5rem; border-radius: 0.375rem; background: rgba(99,102,241,0.15); display: flex; align-items: center; padding: 0 0.75rem; }
|
||||
.rl-zoom-bar__name { font-size: 0.75rem; font-weight: 600; color: var(--rs-text-primary); white-space: nowrap; }
|
||||
.rl-zoom-bar__span { font-size: 0.6rem; color: var(--rs-text-muted); margin-left: auto; white-space: nowrap; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.rapp-info-panel {
|
||||
top: auto; left: 8px; right: 8px; bottom: 8px;
|
||||
transform: none; width: auto; max-height: 85vh;
|
||||
animation: rapp-info-in-mobile 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
@keyframes rapp-info-in-mobile {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.rapp-info-panel__body .rl-grid-2,
|
||||
.rapp-info-panel__body .rl-grid-3,
|
||||
.rapp-info-panel__body .rl-grid-4 { grid-template-columns: 1fr !important; }
|
||||
.rl-hero { padding: 3rem 1rem 2rem; }
|
||||
.rl-hero .rl-heading { font-size: 2rem; }
|
||||
.rl-section { padding: 2.5rem 1rem; }
|
||||
}
|
||||
`;
|
||||
|
||||
export default function InfoPopup({ appName, appIcon, landingHtml, storageKey }: InfoPopupProps) {
|
||||
const key = storageKey || `${appName}-info-seen`;
|
||||
const [open, setOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
try {
|
||||
if (!localStorage.getItem(key)) {
|
||||
setTimeout(() => setOpen(true), 800);
|
||||
localStorage.setItem(key, "1");
|
||||
}
|
||||
} catch {}
|
||||
}, [key]);
|
||||
|
||||
const close = useCallback(() => setOpen(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") close();
|
||||
};
|
||||
document.addEventListener("keydown", handler);
|
||||
return () => document.removeEventListener("keydown", handler);
|
||||
}, [open, close]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div className="info-popup-scope">
|
||||
<style dangerouslySetInnerHTML={{ __html: CSS }} />
|
||||
|
||||
{/* Fixed info button */}
|
||||
<button
|
||||
className="info-popup-trigger"
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label={`About ${appName}`}
|
||||
title={`About ${appName}`}
|
||||
>
|
||||
ⓘ
|
||||
</button>
|
||||
|
||||
{/* Modal */}
|
||||
{open && (
|
||||
<>
|
||||
<div className="rapp-info-overlay" onClick={close} />
|
||||
<div className="rapp-info-panel">
|
||||
<div className="rapp-info-panel__header">
|
||||
<div className="rapp-info-panel__header-left">
|
||||
<span className="rapp-info-panel__icon">{appIcon}</span>
|
||||
<span className="rapp-info-panel__title">{appName}</span>
|
||||
</div>
|
||||
<button className="rapp-info-panel__close" onClick={close} aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="rapp-info-panel__body"
|
||||
dangerouslySetInnerHTML={{ __html: landingHtml }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,429 @@
|
|||
export const LANDING_HTML = `
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="rl-hero">
|
||||
<span class="rl-tagline">rNotes</span>
|
||||
<h1 class="rl-heading" style="background:linear-gradient(135deg,#f59e0b,#f97316);-webkit-background-clip:text;background-clip:text">(You)rNotes, your thoughts unbound.</h1>
|
||||
<p class="rl-subtitle" style="background:linear-gradient(135deg,#f59e0b,#f97316);-webkit-background-clip:text;background-clip:text">Capture Everything, Find Anything, and Share your Insights</p>
|
||||
<p class="rl-subtext">
|
||||
Notes, clips, voice recordings, and live transcription — all in one place.
|
||||
Speak and watch your words appear in real time, or drop in audio and video files to transcribe offline.
|
||||
</p>
|
||||
<div class="rl-cta-row">
|
||||
<a href="https://demo.rspace.online/rnotes" class="rl-cta-primary" id="ml-primary">Open Notebook</a>
|
||||
<a href="#transcription-demo" class="rl-cta-secondary">Transcribe</a>
|
||||
<a href="#extension-download" class="rl-cta-secondary">Get Extension</a>
|
||||
</div>
|
||||
<p style="font-size:0.82rem;margin-top:0.5rem">
|
||||
<a href="#" onclick="document.querySelector('folk-notes-app')?.startTour?.();window.__rspaceHideInfo?.();return false" style="color:var(--rs-primary,#06b6d4);text-decoration:none">
|
||||
Start Guided Tour →
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Live Transcription Demo -->
|
||||
<section class="rl-section" id="transcription-demo">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Live Transcription Demo</h2>
|
||||
<p class="rl-subtext" style="text-align:center">Try it right here — click the mic and start speaking.</p>
|
||||
|
||||
<div style="max-width:640px;margin:2rem auto;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);border-radius:1rem;padding:1.5rem;position:relative">
|
||||
<!-- Unsupported fallback (hidden by default, shown via JS) -->
|
||||
<div id="transcription-unsupported" style="display:none;text-align:center;padding:2rem 1rem;color:#94a3b8">
|
||||
<div style="font-size:2rem;margin-bottom:0.75rem">⚠️</div>
|
||||
<p style="margin:0 0 0.5rem">Live transcription requires <strong>Chrome</strong> or <strong>Edge</strong> (Web Speech API).</p>
|
||||
<p style="margin:0;font-size:0.85rem;color:#64748b">Try opening this page in a Chromium-based browser to test the demo.</p>
|
||||
</div>
|
||||
|
||||
<!-- Demo UI (hidden if unsupported) -->
|
||||
<div id="transcription-ui">
|
||||
<!-- Controls -->
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:1rem;margin-bottom:1.25rem">
|
||||
<button id="mic-btn" style="width:56px;height:56px;border-radius:50%;border:2px solid rgba(245,158,11,0.4);background:rgba(245,158,11,0.1);color:#f59e0b;font-size:1.5rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s" title="Start transcription">
|
||||
🎤
|
||||
</button>
|
||||
<div style="text-align:left">
|
||||
<div id="mic-status" style="font-size:0.9rem;color:#94a3b8">Click mic to start</div>
|
||||
<div id="mic-timer" style="font-size:0.75rem;color:#64748b;font-variant-numeric:tabular-nums">00:00</div>
|
||||
</div>
|
||||
<div id="live-indicator" style="display:none;background:rgba(239,68,68,0.15);color:#ef4444;font-size:0.7rem;font-weight:600;padding:0.2rem 0.6rem;border-radius:9999px;text-transform:uppercase;letter-spacing:0.05em">
|
||||
● Live
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transcript area -->
|
||||
<div id="transcript-area" style="min-height:120px;max-height:240px;overflow-y:auto;background:rgba(0,0,0,0.2);border-radius:0.5rem;padding:1rem;font-size:0.9rem;line-height:1.6;color:#e2e8f0">
|
||||
<span style="color:#64748b;font-style:italic">Your transcript will appear here…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Capability badges -->
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;justify-content:center;margin-top:1.25rem">
|
||||
<span class="rl-badge" style="background:rgba(34,197,94,0.15);color:#22c55e">● Live streaming</span>
|
||||
<span class="rl-badge" style="background:rgba(59,130,246,0.15);color:#3b82f6">🎵 Audio file upload</span>
|
||||
<span class="rl-badge" style="background:rgba(168,85,247,0.15);color:#a855f7">🎥 Video transcription</span>
|
||||
<span class="rl-badge" style="background:rgba(245,158,11,0.15);color:#f59e0b">🔌 Offline (Parakeet.js)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Transcription Demo Script -->
|
||||
<script>
|
||||
(function() {
|
||||
var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
var ui = document.getElementById('transcription-ui');
|
||||
var unsupported = document.getElementById('transcription-unsupported');
|
||||
if (!SpeechRecognition) {
|
||||
if (ui) ui.style.display = 'none';
|
||||
if (unsupported) unsupported.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
var recognition = new SpeechRecognition();
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
recognition.lang = 'en-US';
|
||||
|
||||
var micBtn = document.getElementById('mic-btn');
|
||||
var micStatus = document.getElementById('mic-status');
|
||||
var micTimer = document.getElementById('mic-timer');
|
||||
var liveIndicator = document.getElementById('live-indicator');
|
||||
var transcriptArea = document.getElementById('transcript-area');
|
||||
|
||||
var isListening = false;
|
||||
var timerInterval = null;
|
||||
var seconds = 0;
|
||||
var finalTranscript = '';
|
||||
|
||||
function formatTime(s) {
|
||||
var m = Math.floor(s / 60);
|
||||
var sec = s % 60;
|
||||
return (m < 10 ? '0' : '') + m + ':' + (sec < 10 ? '0' : '') + sec;
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
seconds = 0;
|
||||
micTimer.textContent = '00:00';
|
||||
timerInterval = setInterval(function() {
|
||||
seconds++;
|
||||
micTimer.textContent = formatTime(seconds);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||||
}
|
||||
|
||||
micBtn.addEventListener('click', function() {
|
||||
if (!isListening) {
|
||||
finalTranscript = '';
|
||||
transcriptArea.innerHTML = '';
|
||||
recognition.start();
|
||||
} else {
|
||||
recognition.stop();
|
||||
}
|
||||
});
|
||||
|
||||
recognition.onstart = function() {
|
||||
isListening = true;
|
||||
micBtn.style.background = 'rgba(239,68,68,0.2)';
|
||||
micBtn.style.borderColor = '#ef4444';
|
||||
micBtn.style.color = '#ef4444';
|
||||
micBtn.title = 'Stop transcription';
|
||||
micStatus.textContent = 'Listening...';
|
||||
micStatus.style.color = '#ef4444';
|
||||
liveIndicator.style.display = 'block';
|
||||
startTimer();
|
||||
};
|
||||
|
||||
recognition.onend = function() {
|
||||
isListening = false;
|
||||
micBtn.style.background = 'rgba(245,158,11,0.1)';
|
||||
micBtn.style.borderColor = 'rgba(245,158,11,0.4)';
|
||||
micBtn.style.color = '#f59e0b';
|
||||
micBtn.title = 'Start transcription';
|
||||
micStatus.textContent = 'Click mic to start';
|
||||
micStatus.style.color = '#94a3b8';
|
||||
liveIndicator.style.display = 'none';
|
||||
stopTimer();
|
||||
};
|
||||
|
||||
recognition.onresult = function(event) {
|
||||
var interim = '';
|
||||
for (var i = event.resultIndex; i < event.results.length; i++) {
|
||||
var transcript = event.results[i][0].transcript;
|
||||
if (event.results[i].isFinal) {
|
||||
finalTranscript += transcript + ' ';
|
||||
} else {
|
||||
interim += transcript;
|
||||
}
|
||||
}
|
||||
transcriptArea.innerHTML = finalTranscript +
|
||||
(interim ? '<span style="color:#94a3b8">' + interim + '</span>' : '');
|
||||
transcriptArea.scrollTop = transcriptArea.scrollHeight;
|
||||
};
|
||||
|
||||
recognition.onerror = function(event) {
|
||||
if (event.error === 'not-allowed') {
|
||||
micStatus.textContent = 'Microphone access denied';
|
||||
micStatus.style.color = '#ef4444';
|
||||
}
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="rl-section">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">What rNotes Handles</h2>
|
||||
<div class="rl-grid-3">
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">📝</div>
|
||||
<h3>Rich Text Notes</h3>
|
||||
<p>Write with a full TipTap editor — formatting, code blocks, checklists, and embeds. Dual-format storage keeps Markdown portable.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🎤</div>
|
||||
<h3>Voice & Transcription</h3>
|
||||
<p>Record voice notes with live transcription via Web Speech API. Drop in audio or video files and get full transcripts — all in the browser.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🏷</div>
|
||||
<h3>Tagging & Organization</h3>
|
||||
<p>Tag freely, organize into notebooks, and search everything. Filtered views surface the right cards at the right time.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Chrome Extension -->
|
||||
<section class="rl-section rl-section--alt" id="extension-download">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Chrome Extension</h2>
|
||||
<p class="rl-subtext" style="text-align:center">Clip pages, record voice notes, and transcribe — right from the toolbar.</p>
|
||||
<div class="rl-grid-2" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1.5rem;max-width:860px;margin:2rem auto">
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">📋</div>
|
||||
<h3>Web Clipper</h3>
|
||||
<p>Save any page as a note with one click — article text, selection, or full HTML.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🎤</div>
|
||||
<h3>Voice Recording</h3>
|
||||
<p>Press <kbd style="background:rgba(255,255,255,0.1);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.8rem">Ctrl+Shift+V</kbd> to start recording and transcribing from any tab.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🔓</div>
|
||||
<h3>Article Unlock</h3>
|
||||
<p>Bypass soft paywalls by fetching archived versions — read the article, then save it to your notebook.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🔌</div>
|
||||
<h3>Offline Transcription</h3>
|
||||
<p>Parakeet.js runs entirely in-browser — your audio never leaves the device.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:1.5rem">
|
||||
<a href="/rnotes/extension/download" class="rl-cta-primary" style="display:inline-flex;align-items:center;gap:0.5rem">
|
||||
⬇ Download Extension
|
||||
</a>
|
||||
<p style="margin-top:0.75rem;font-size:0.8rem;color:#64748b">
|
||||
Unzip, then load unpacked at <code style="font-size:0.75rem;color:#94a3b8">chrome://extensions</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works -->
|
||||
<section class="rl-section">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
|
||||
<div class="rl-grid-4" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem">
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">1</div>
|
||||
<h3>Live Transcribe</h3>
|
||||
<p>Speak and watch words appear in real time via the Web Speech API. No uploads, no waiting.</p>
|
||||
</div>
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">2</div>
|
||||
<h3>Audio & Video</h3>
|
||||
<p>Drop files and get full transcripts via Parakeet, running entirely in-browser. Supports MP3, WAV, MP4, and more.</p>
|
||||
</div>
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">3</div>
|
||||
<h3>Notebooks & Tags</h3>
|
||||
<p>Organize transcripts alongside notes, clips, code, and files. Tag, search, and filter across everything.</p>
|
||||
</div>
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">4</div>
|
||||
<h3>Private & Offline</h3>
|
||||
<p>Parakeet.js runs in-browser — audio never leaves your device. Works offline once the model is cached.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Memory Cards -->
|
||||
<section class="rl-section rl-section--alt">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Memory Cards</h2>
|
||||
<p class="rl-subtext" style="text-align:center">
|
||||
Every note is a Memory Card — a typed, structured unit of knowledge with hierarchy,
|
||||
properties, and attachments. Designed for round-trip interoperability with Logseq.
|
||||
</p>
|
||||
<div class="rl-grid-3">
|
||||
<!-- 7 Card Types -->
|
||||
<div class="rl-card">
|
||||
<div class="rl-icon-box">🏷</div>
|
||||
<h3>7 Card Types</h3>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.35rem;margin-bottom:0.75rem">
|
||||
<span class="rl-badge" style="background:rgba(245,158,11,0.2);color:#f59e0b">note</span>
|
||||
<span class="rl-badge" style="background:rgba(59,130,246,0.2);color:#3b82f6">link</span>
|
||||
<span class="rl-badge" style="background:rgba(34,197,94,0.2);color:#22c55e">task</span>
|
||||
<span class="rl-badge" style="background:rgba(234,179,8,0.2);color:#eab308">idea</span>
|
||||
<span class="rl-badge" style="background:rgba(168,85,247,0.2);color:#a855f7">person</span>
|
||||
<span class="rl-badge" style="background:rgba(236,72,153,0.2);color:#ec4899">reference</span>
|
||||
<span class="rl-badge" style="background:rgba(100,116,139,0.2);color:#94a3b8">file</span>
|
||||
</div>
|
||||
<p>Each card type has distinct styling and behavior. Typed notes surface in filtered views and canvas visualizations.</p>
|
||||
</div>
|
||||
|
||||
<!-- Hierarchy & Properties -->
|
||||
<div class="rl-card">
|
||||
<div class="rl-icon-box">📂</div>
|
||||
<h3>Hierarchy & Properties</h3>
|
||||
<p>
|
||||
Nest cards under parents to build knowledge trees. Add structured
|
||||
<code style="font-size:0.8rem;color:rgba(245,158,11,0.8)">key:: value</code>
|
||||
properties — compatible with Logseq's property syntax.
|
||||
</p>
|
||||
<div style="margin-top:0.5rem;font-family:monospace;font-size:0.75rem;color:#64748b;line-height:1.7">
|
||||
<div><span style="color:#94a3b8">type::</span> idea</div>
|
||||
<div><span style="color:#94a3b8">status::</span> doing</div>
|
||||
<div><span style="color:#94a3b8">tags::</span> #research, #web3</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Source Integrations -->
|
||||
<div class="rl-card">
|
||||
<div class="rl-icon-box">🔄</div>
|
||||
<h3>Import & Export</h3>
|
||||
<p>
|
||||
Bring your notes from <strong>Logseq</strong>, <strong>Obsidian</strong>,
|
||||
<strong>Notion</strong>, and <strong>Google Docs</strong>.
|
||||
Export back to any format anytime — your data, your choice.
|
||||
</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.35rem;margin-top:0.5rem">
|
||||
<span class="rl-badge" style="background:rgba(34,197,94,0.2);color:#22c55e">Logseq</span>
|
||||
<span class="rl-badge" style="background:rgba(139,92,246,0.2);color:#8b5cf6">Obsidian</span>
|
||||
<span class="rl-badge" style="background:rgba(59,130,246,0.2);color:#3b82f6">Notion</span>
|
||||
<span class="rl-badge" style="background:rgba(245,158,11,0.2);color:#f59e0b">Google Docs</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dual Format Storage -->
|
||||
<div class="rl-card">
|
||||
<div class="rl-icon-box">📄</div>
|
||||
<h3>Dual Format Storage</h3>
|
||||
<p>
|
||||
Every card stores rich TipTap JSON for editing and portable Markdown for search, export, and interoperability.
|
||||
Write once, read anywhere.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Structured Attachments -->
|
||||
<div class="rl-card">
|
||||
<div class="rl-icon-box">📎</div>
|
||||
<h3>Structured Attachments</h3>
|
||||
<p>
|
||||
Attach images, PDFs, audio, and files to any card with roles (primary, preview, supporting) and captions.
|
||||
Thumbnails render inline.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- FUN Model -->
|
||||
<div class="rl-card">
|
||||
<div class="rl-icon-box">❤</div>
|
||||
<h3>FUN, Not CRUD</h3>
|
||||
<p>
|
||||
<span style="color:#f59e0b;font-weight:600">F</span>orget,
|
||||
<span style="color:#f59e0b;font-weight:600">U</span>pdate,
|
||||
<span style="color:#f59e0b;font-weight:600">N</span>ew —
|
||||
nothing is permanently destroyed. Forgotten cards are archived and can be remembered at any time.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Built on Open Source -->
|
||||
<section class="rl-section">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Built on Open Source</h2>
|
||||
<p class="rl-subtext" style="text-align:center">The libraries and tools that power rNotes.</p>
|
||||
<div class="rl-grid-4" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem">
|
||||
<div class="rl-card rl-card--center">
|
||||
<h3>Automerge</h3>
|
||||
<p>Local-first CRDT for conflict-free real-time collaboration. Your notes sync across devices without a central server.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<h3>Web Speech API</h3>
|
||||
<p>Browser-native live transcription — speak and watch your words appear in real time.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<h3>Parakeet.js</h3>
|
||||
<p>NVIDIA’s in-browser speech recognition. Transcribe audio and video files offline — nothing leaves your device.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<h3>Hono</h3>
|
||||
<p>Ultra-fast, lightweight API framework powering the rNotes backend.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Your Data, Protected -->
|
||||
<section class="rl-section rl-section--alt">
|
||||
<div class="rl-container" style="text-align:center">
|
||||
<h2 class="rl-heading">Your Data, Protected</h2>
|
||||
<p class="rl-subtext">How rNotes keeps your information safe.</p>
|
||||
<div class="rl-grid-3">
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🔒</div>
|
||||
<h3>End-to-End Encryption</h3>
|
||||
<span class="rl-badge">Coming Soon</span>
|
||||
<p>All content encrypted before it leaves your device. Not even the server can read it.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🕵</div>
|
||||
<h3>Zero-Knowledge Architecture</h3>
|
||||
<span class="rl-badge">Coming Soon</span>
|
||||
<p>The server processes your requests without ever seeing your data in the clear.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🏠</div>
|
||||
<h3>Self-Hosted</h3>
|
||||
<p>Run on your own infrastructure. Your server, your rules, your data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="rl-section">
|
||||
<div class="rl-container" style="text-align:center">
|
||||
<h2 class="rl-heading">(You)rNotes, your thoughts unbound.</h2>
|
||||
<p class="rl-subtext">Try the demo or create a space to get started.</p>
|
||||
<div class="rl-cta-row">
|
||||
<a href="https://demo.rspace.online/rnotes" class="rl-cta-primary">Open Notebook</a>
|
||||
<a href="#transcription-demo" class="rl-cta-secondary">Transcribe</a>
|
||||
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="rl-back">
|
||||
<a href="/">← Back to rSpace</a>
|
||||
</div>
|
||||
`;
|
||||
Loading…
Reference in New Issue