This commit is contained in:
despiegk 2024-11-15 10:49:55 +03:00
parent cbe5e76842
commit dbfec80d3f
80 changed files with 3684 additions and 2 deletions

View File

@ -7,7 +7,14 @@ import os
import markdown import markdown
import re import re
sources_dir = os.path.expanduser("~/code/git.ourworld.tf/tfgrid/www_projectmycelium/poc") # Get BASE_DIR from environment variables with a default fallback
BASE_DIR = os.environ.get('BASE_DIR', os.path.dirname(os.path.abspath(__file__)))
# Check if BASE_DIR exists
if not os.path.exists(BASE_DIR):
raise RuntimeError(f"The BASE_DIR '{BASE_DIR}' does not exist.")
sources_dir = os.path.expanduser(f"{BASE_DIR}/poc")
if not os.path.exists(sources_dir): if not os.path.exists(sources_dir):
raise RuntimeError(f"The source directory '{sources_dir}' does not exist.") raise RuntimeError(f"The source directory '{sources_dir}' does not exist.")
static_dir = f"{sources_dir}/static" static_dir = f"{sources_dir}/static"

View File

Before

Width:  |  Height:  |  Size: 602 KiB

After

Width:  |  Height:  |  Size: 602 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 345 B

View File

Before

Width:  |  Height:  |  Size: 293 KiB

After

Width:  |  Height:  |  Size: 293 KiB

View File

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 702 KiB

View File

Before

Width:  |  Height:  |  Size: 387 KiB

After

Width:  |  Height:  |  Size: 387 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -0,0 +1,11 @@
<section class="faq-container">
<div class="markdown-content">
[[{{ config["name"] }}]]
</div>
<div class="faq-section">
<h4>Frequently Asked Questions</h4>
[[{{ config["section_name"] }}]]
</div>
</section>

View File

@ -0,0 +1,206 @@
{% include 'components/login.html' %}
<footer class="tf_footer">
<div class="tf_footer_container">
<div class="tf_footer_section">
<h3>ThreeFold</h3>
<p>Building a decentralized internet <br>for a better world.</p>
</div>
<div class="tf_footer_section">
<h4>Links</h4>
<div class="footer_links">
<a href="#">About</a>
<a href="#">Technology</a>
<a href="#">Community</a>
<a href="#">Contact</a>
</div>
</div>
<div class="tf_footer_section">
<h4>Resources</h4>
<div class="footer_links">
<a href="#">Documentation</a>
<a href="#">Blog</a>
<a href="#">FAQ</a>
<a href="#">Support</a>
</div>
</div>
<div class="tf_footer_section">
<h4>Connect</h4>
<div class="footer_links">
<a href="#">Twitter</a>
<a href="#">Telegram</a>
<a href="#">GitHub</a>
<a href="#">Discord</a>
</div>
</div>
</div>
<div class="tf_footer_bottom">
<p>&copy; 2024 ThreeFold. All rights reserved.</p>
</div>
</footer>
<style>
:root {
/* Light theme variables */
--footer-background-light: #FFFFFFF2;
--footer-text-light: #333;
--footer-link-light: #385bb5;
--footer-link-hover-light: #2a4494;
--footer-border-light: #eee;
/* Dark theme variables */
--footer-background-dark: #282C34F2;
--footer-text-dark: #fff;
--footer-link-dark: #7a9bff;
--footer-link-hover-dark: #a8beff;
--footer-border-dark: #444;
/* Default to dark theme */
--footer-background: var(--footer-background-dark);
--footer-text: var(--footer-text-dark);
--footer-link: var(--footer-link-dark);
--footer-link-hover: var(--footer-link-hover-dark);
--footer-border: var(--footer-border-dark);
}
/* Light theme class */
.light-theme {
--footer-background: var(--footer-background-light);
--footer-text: var(--footer-text-light);
--footer-link: var(--footer-link-light);
--footer-link-hover: var(--footer-link-hover-light);
--footer-border: var(--footer-border-light);
}
.tf_footer {
background-color: var(--footer-background);
color: var(--footer-text);
padding: 1.5rem 0 0.5rem;
margin-top: auto;
transition: background-color 0.3s, color 0.3s;
box-sizing: border-box;
}
.tf_footer_container {
max-width: 1200px;
margin: 0 auto;
padding: 0 0rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.8rem;
}
.tf_footer_section {
display: flex;
flex-direction: column;
}
.tf_footer_section h3 {
font-size: 1.1rem;
margin: 0 0 0.4rem;
color: var(--footer-text);
}
.tf_footer_section h4 {
font-size: 0.9rem;
margin: 0 0 0.3rem;
color: var(--footer-text);
padding-left: 1rem;
}
.tf_footer_section p {
margin: 0;
line-height: 1.3;
font-size: 0.8rem;
color: var(--footer-text);
}
.footer_links {
display: flex;
flex-direction: column;
gap: 0.2rem;
padding-left: 1rem;
}
.footer_links a {
color: var(--footer-link);
text-decoration: none;
transition: color 0.3s;
font-size: 0.8rem;
line-height: 1.2;
}
.footer_links a:hover {
color: var(--footer-link-hover);
}
.tf_footer_bottom {
background-color: var(--footer-background);
margin-top: 1rem;
padding-top: 0.5rem;
/* Add flexbox properties */
display: flex;
justify-content: center; /* Horizontally center */
align-items: center; /* Vertically center */
border-top: 0px solid var(--footer-border);
}
.tf_footer_bottom p {
margin: 0;
font-size: 0.75rem;
color: var(--footer-text);
}
@media (max-width: 768px) {
.tf_footer_container {
grid-template-columns: repeat(2, 1fr);
gap: 0.8rem;
}
.tf_footer {
padding: 1rem 0 0.4rem;
}
}
@media (max-width: 480px) {
.tf_footer_container {
grid-template-columns: repeat(2, 1fr);
}
.tf_footer {
padding: 0.8rem 0 0.3rem;
}
.tf_footer_section {
font-size: 0.75rem;
}
.tf_footer_section h4 {
padding-left: 0.3rem;
}
.footer_links {
padding-left: 0.6rem;
}
}
</style>
<script>
// Ensure theme changes are applied consistently across all components
function applyTheme(theme) {
document.documentElement.className = theme;
localStorage.setItem('theme', theme);
}
// Initialize theme from localStorage or default to dark
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme') || '';
applyTheme(savedTheme);
});
// Override the toggleTheme function from nav.html
function toggleTheme() {
const isLight = document.documentElement.classList.contains('light-theme');
applyTheme(isLight ? '' : 'light-theme');
}
</script>

View File

@ -0,0 +1,62 @@
<div class="globe-container">
<canvas
id="cobe"
style="width: 500px; height: 500px"
width="1000"
height="1000"
></canvas>
</div>
<style>
.globe-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 20px 0;
}
</style>
<script type="module">
import createGlobe from 'https://cdn.skypack.dev/cobe'
let phi = 0
let canvas = document.getElementById("cobe")
// Detect system dark mode
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const globe = createGlobe(canvas, {
devicePixelRatio: 2,
width: 1000,
height: 1000,
phi: 0,
theta: 0.3,
dark: isDarkMode ? 1 : 0,
diffuse: 1.2,
scale: 1,
mapSamples: 16000,
mapBrightness: isDarkMode ? 15 : 4,
baseColor: isDarkMode ? [0.8, 0.8, 0.8] : [0.3, 0.3, 0.3],
markerColor: [0.1, 0.8, 1],
glowColor: isDarkMode ? [0.6, 0.6, 0.6] : [0.2, 0.2, 0.2],
offset: [0, 0],
markers: [
{ location: [37.7595, -122.4367], size: 0.03 },
{ location: [40.7128, -74.006], size: 0.1 },
],
onRender: (state) => {
state.phi = phi
phi += 0.005
},
})
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
const newIsDarkMode = e.matches
globe.dark = newIsDarkMode ? 1 : 0
globe.baseColor = newIsDarkMode ? [0.8, 0.8, 0.8] : [0.3, 0.3, 0.3]
globe.mapBrightness = newIsDarkMode ? 15 : 4
globe.glowColor = newIsDarkMode ? [0.6, 0.6, 0.6] : [0.2, 0.2, 0.2]
})
</script>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en" x-data>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Decentralized Internet</title>
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="/static/css/ourworld.css">
<link rel="stylesheet" href="/static/css/menu.css">
<link rel="stylesheet" href="/static/css/login.css">
<link rel="stylesheet" href="/static/css/faq.css">
<!-- Load Alpine.js from CDN -->
<script defer src="https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js"></script>
</head>

View File

@ -0,0 +1,133 @@
<div class="hero2_container" x-data="{ isZoomingIn: true }" x-init="setInterval(() => isZoomingIn = !isZoomingIn, 15000)">
<!-- Text Box -->
<div class="hero2_text-box">
<h2 class="hero2_title"> {{config["title"]}} </h2>
<p class="hero2_subtitle"> {{config["subtitle"]}} </p>
</div>
<!-- Image Wrapper -->
<div class="hero2_image-wrapper">
<img src="{{config["image"]}}"
class="hero2_floating-image"
:style="isZoomingIn ? 'transform: scale(1.5)' : 'transform: scale(1)'"
style="transition: transform 20s ease-in">
</div>
</div>
<style>
main {
background-color: var(--body-background);
transition: background-color 0.3s;
}
.hero2_container {
position: relative;
padding: 3rem;
background-color: var(--modal-background);
border-radius: 1.5rem;
box-shadow: 0 8px 32px #00000026;
min-height: 400px;
width: 100%;
max-width: 1200px;
overflow: hidden;
display: flex;
align-items: center;
margin: 2rem auto;
transition: all 0.3s ease;
}
.hero2_text-box {
position: relative;
z-index: 3;
max-width: 500px;
padding-right: 2rem;
}
.hero2_title {
font-size: clamp(1.5rem, 5vw, 2.5rem);
font-weight: bold;
color: var(--text-color);
margin: 0;
text-shadow: 0 1px 2px #0000001A;
line-height: 1.2;
transition: color 0.3s;
}
.hero2_subtitle {
margin-top: 1rem;
background-color: #385bb5;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
display: inline-block;
box-shadow: 0 2px 4px #0000001A;
font-size: clamp(0.875rem, 2vw, 1rem);
max-width: 100%;
transition: all 0.3s ease;
}
.light-theme .hero2_subtitle {
background-color: #385bb5;
color: white;
}
.hero2_image-wrapper {
position: absolute;
top: 50%;
right: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
z-index: 2;
pointer-events: none;
transform: translateY(-50%);
}
.hero2_floating-image {
width: 1000px;
max-width: 100%;
height: auto;
transition: transform 15s ease-in;
margin-right: -100px;
transform-origin: center center;
}
[x-cloak] {
display: none !important;
}
@media (max-width: 768px) {
.hero2_container {
padding: 2rem;
min-height: 250px;
width: 90%;
margin: 1rem auto;
}
.hero2_text-box {
padding-right: 0;
max-width: 100%;
}
.hero2_image-wrapper {
opacity: 0.4;
}
.hero2_floating-image {
margin-right: -200px;
}
.hero2_subtitle {
margin-top: 0.75rem;
}
}
@media (max-width: 480px) {
.hero2_container {
padding: 1.2rem;
min-height: 200px;
width: 85%;
}
.hero2_floating-image {
margin-right: -200px;
}
}
</style>

View File

@ -0,0 +1,98 @@
<div class="hero1">
<div class="hero1_box">
<h1 class="hero1_title"></h1>
<p class="hero1_subtitle">[[{{ config["subtitle"] }}]]</p>
</div>
<div class="hero1_banner"> </div>
</div>
<style>
.hero1 {
position: relative;
padding: 2.5rem;
background-color: var(--hero-background);
border-radius: 1.2rem;
box-shadow: 0 8px 32px #0000004D;
min-height: 400px;
width: 100%;
max-width: 1200px;
overflow: hidden;
display: flex;
align-items: center;
gap: 2.5rem;
margin: 2rem auto;
color: var(--hero-text);
}
.hero1_box {
position: relative;
z-index: 3;
max-width: 500px;
flex-shrink: 1;
}
.hero1_title {
font-size: clamp(1.5rem, 5vw, 2.5rem);
font-weight: bold;
color: var(--hero-text);
margin: 0;
text-shadow: 0 2px 4px #00000033;
line-height: 1.2;
}
.hero1_subtitle {
margin-top: 1rem;
background-color: var(--hero-subtitle-background);
color: var(--hero-subtitle-text);
padding: 0.4rem 0.8rem;
border-radius: 0.4rem;
display: inline-block;
text-shadow: 0 1px 2px #00000033;
box-shadow: 0 2px 4px #00000033;
font-size: clamp(0.875rem, 2vw, 1rem);
max-width: 100%;
}
.hero1_banner {
flex-grow: 1;
background-color: var(--hero-banner-background);
padding: 1.3rem;
border-radius: 0.8rem;
line-height: 1.4;
font-size: 1rem;
min-height: 300px;
color: var(--hero-text);
}
@media (max-width: 768px) {
.hero1 {
padding: 1.6rem;
min-height: 250px;
width: 90%;
flex-direction: column;
}
.hero1_box {
padding-right: 0;
max-width: 100%;
}
.hero1_subtitle {
margin-top: 0.75rem;
}
.hero1_banner {
padding: 1.2rem;
}
}
@media (max-width: 480px) {
.hero1_banner {
font-size: 0.9rem;
padding: 1rem;
}
.hero1 {
padding: 1rem;
min-height: 200px;
width: 85%;
}
}
</style>

View File

@ -0,0 +1,217 @@
<div class="modal-overlay" id="identifyModal">
<div class="modal-container">
<div class="modal-header">
<h2>Identify Yourself</h2>
<p class="modal-description">Enter your secret password to access the key management system.</p>
</div>
<form class="modal-form" id="identifyForm" onsubmit="return handleIdentify(event)">
<div class="form-group">
<label for="secret">Secret Password</label>
<input type="password" id="secret" name="secret" required
placeholder="Enter your secret password"
autocomplete="current-password">
<div class="error-message" id="errorMessage"></div>
</div>
<div class="modal-buttons">
<button type="button" class="btn btn-secondary" onclick="closeIdentifyModal()">Cancel</button>
<button type="submit" class="btn btn-primary" id="identifyButton">Identify</button>
</div>
</form>
</div>
</div>
<script>
function showIdentifyModal() {
document.getElementById('identifyModal').style.display = 'block';
document.getElementById('secret').focus();
}
function closeIdentifyModal() {
document.getElementById('identifyModal').style.display = 'none';
document.getElementById('identifyForm').reset();
document.getElementById('errorMessage').style.display = 'none';
}
async function handleIdentify(event) {
event.preventDefault();
const secret = document.getElementById('secret').value;
const errorMessage = document.getElementById('errorMessage');
const identifyButton = document.getElementById('identifyButton');
// Disable button during processing
identifyButton.disabled = true;
identifyButton.textContent = 'Processing...';
try {
if (!window.kvs) {
throw new Error('Key management system not initialized');
}
// Set the password in session storage
window.kvs.setPassword(secret);
// Try to generate/retrieve the private key to verify the password
await window.kvs.generate_private_key();
// If successful, close the modal
closeIdentifyModal();
// Dispatch an event to notify that identification was successful
window.dispatchEvent(new CustomEvent('userIdentified'));
} catch (error) {
console.error('Identification error:', error);
errorMessage.textContent = error.message || 'Invalid secret password';
errorMessage.style.display = 'block';
window.kvs.clearSession();
} finally {
// Re-enable button
identifyButton.disabled = false;
identifyButton.textContent = 'Identify';
}
return false;
}
// Check session validity periodically
const sessionCheckInterval = setInterval(() => {
if (window.kvs && !window.kvs.isSessionValid()) {
window.kvs.clearSession();
showIdentifyModal();
}
}, 60000); // Check every minute
// Cleanup interval when page unloads
window.addEventListener('unload', () => {
clearInterval(sessionCheckInterval);
});
// Initial check when the page loads
window.addEventListener('DOMContentLoaded', () => {
if (!window.kvs || !window.kvs.isSessionValid()) {
showIdentifyModal();
}
});
</script>
<style>
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
backdrop-filter: blur(3px);
}
.modal-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 1001;
width: 90%;
max-width: 400px;
}
.modal-header {
margin-bottom: 1.5rem;
text-align: center;
}
.modal-description {
color: #666;
margin-top: 0.5rem;
font-size: 0.9rem;
}
.modal-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: bold;
color: #333;
}
.form-group input {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1.5rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
font-weight: 500;
}
.btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background-color: #545b62;
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.5rem;
display: none;
padding: 0.5rem;
background-color: #fff5f5;
border-radius: 4px;
border: 1px solid #ffebee;
}
</style>

View File

@ -0,0 +1,123 @@
<!-- Login Modal -->
<div id="loginModal" class="modal">
<div class="modal-content">
<div class="login-box">
<div class="login-header">
<svg class="logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
<h2>Welcome Back</h2>
<p>Sign in to your ThreeFold account</p>
</div>
<form id="login-form" onsubmit="return validateLoginForm(event)">
<div class="form-group">
<label for="login-email">Email</label>
<input
type="email"
id="login-email"
name="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
placeholder="Enter your email"
autocomplete="email"
>
<span class="error-message" id="login-emailError"></span>
</div>
<div class="form-group">
<label for="login-password">Password</label>
<input
type="password"
id="login-password"
name="password"
required
minlength="8"
placeholder="Enter your password"
autocomplete="current-password"
>
<span class="error-message" id="login-passwordError"></span>
</div>
<div class="form-footer">
<label class="remember-me">
<input type="checkbox" name="remember"> Remember me
</label>
<a href="#" class="forgot-password">Forgot Password?</a>
</div>
<button type="submit" class="submit-button">Sign In</button>
</form>
<div class="signup-link">
Don't have an account? <a href="#" onclick="openSignupModal(); closeLoginModal();">Sign up</a>
</div>
<button class="close-button" onclick="closeLoginModal()">&times;</button>
</div>
</div>
</div>
<script>
function openLoginModal() {
const modal = document.getElementById('loginModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLoginModal() {
const modal = document.getElementById('loginModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('loginModal');
if (event.target === modal) {
closeLoginModal();
}
}
function validateLoginForm(event) {
event.preventDefault();
const email = document.getElementById('login-email');
const emailError = document.getElementById('login-emailError');
const password = document.getElementById('login-password');
const passwordError = document.getElementById('login-passwordError');
// Reset error messages
emailError.textContent = '';
passwordError.textContent = '';
let isValid = true;
// Email validation
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email.value)) {
emailError.textContent = 'Please enter a valid email address';
isValid = false;
}
// Password validation
if (password.value.length < 8) {
passwordError.textContent = 'Password must be at least 8 characters long';
isValid = false;
}
if (isValid) {
// Here you would typically send the form data to your server
console.log('Form is valid, ready to submit');
closeLoginModal();
}
return false; // Prevent form submission
}
// Real-time email validation
document.getElementById('login-email')?.addEventListener('input', function(e) {
const emailError = document.getElementById('login-emailError');
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (this.value && !emailRegex.test(this.value)) {
emailError.textContent = 'Please enter a valid email address';
} else {
emailError.textContent = '';
}
});
</script>

View File

@ -0,0 +1,60 @@
<div class="tf_right_controls">
<button class="tf_signup_btn" onclick="openSignupModal()">Sign Up</button>
<button class="tf_login_btn" onclick="openLoginModal()">Login</button>
<button class="tf_theme_toggle" @click="document.documentElement.classList.toggle('light-theme')">
<svg class="theme-icon light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<svg class="theme-icon dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
</div>
<style>
.tf_right_controls {
display: flex;
align-items: center;
gap: 2rem;
margin-left: 4rem;
}
.tf_signup_btn {
background: transparent;
border: 0.5px solid var(--text-color);
color: var(--text-color);
padding: 0 1rem;
border-radius: 4px;
font-size: 0.8125rem;
cursor: pointer;
transition: all 0.2s;
margin-right: 0.5rem;
height: 28px;
min-width: 80px;
line-height: 28px;
font-weight: normal;
}
.tf_signup_btn:hover {
background: var(--text-color);
color: var(--body-background);
transform: translateY(-1px);
box-shadow: 0 2px 4px #0000001A;
}
.tf_login_btn {
height: 28px;
margin-right: 0.5rem;
min-width: 80px;
line-height: 28px;
padding: 0 1rem;
}
</style>

View File

@ -0,0 +1,25 @@
<div x-data="{ mobileMenuOpen: false }">
<nav class="tf_nav">
<div class="tf_nav_container">
<button class="hamburger-menu" @click="mobileMenuOpen = !mobileMenuOpen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<a href="#" class="tf_logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
<span>ThreeFold</span>
</a>
<ul class="tf_menu" :class="{ 'active': mobileMenuOpen }">
[[navcontent]]
</ul>
{% include 'components/login_lightswitch.html' %}
</div>
</nav>
<div class="mobile-overlay" :class="{ 'active': mobileMenuOpen }" @click="mobileMenuOpen = false"></div>
</div>

View File

@ -0,0 +1,150 @@
<div class="roadmap-container">
<h1>Our Journey</h1>
<div class="timeline">
<div class="milestone">
<div class="milestone-date">2023 Q4</div>
<div class="milestone-content">
<h3>Platform Launch</h3>
<p>Initial release of our decentralized infrastructure and core services.</p>
</div>
</div>
<div class="milestone">
<div class="milestone-date">2024 Q1</div>
<div class="milestone-content">
<h3>Network Expansion</h3>
<p>Global node deployment and enhanced network capabilities.</p>
</div>
</div>
<div class="milestone">
<div class="milestone-date">2024 Q2</div>
<div class="milestone-content">
<h3>Developer Tools</h3>
<p>Release of comprehensive SDK and developer documentation.</p>
</div>
</div>
<div class="milestone">
<div class="milestone-date">2024 Q3</div>
<div class="milestone-content">
<h3>Enterprise Solutions</h3>
<p>Launch of enterprise-grade features and support services.</p>
</div>
</div>
</div>
</div>
<style>
.roadmap-container {
width: 100%;
max-width: 1200px;
margin: 2rem auto;
padding: 2rem 3rem;
background: var(--hero-background);
border-radius: 1.2rem;
box-shadow: 0 8px 32px #0000004D;
}
.roadmap-container h1 {
color: var(--hero-subtitle-background);
text-align: center;
margin-bottom: 2rem;
}
.timeline {
position: relative;
padding: 2rem 0;
}
.timeline::before {
content: '';
position: absolute;
left: 96px;
top: 0;
bottom: 0;
width: 2px;
background: #888;
}
.milestone {
display: flex;
margin-bottom: 2rem;
position: relative;
align-items: center; /* Use center alignment */
}
.milestone::before {
content: '';
position: absolute;
left: 88px;
top: 50%; /* Center the circle vertically */
transform: translateY(-50%); /* Adjust to align perfectly */
width: 18px;
height: 18px;
border-radius: 50%;
background: #888;
border: 2px solid var(--hero-background);
box-sizing: border-box;
}
.milestone-date {
width: 70px;
padding-right: 1rem;
font-family: 'Poppins', sans-serif;
font-weight: 600;
color: var(--hero-subtitle-background);
padding-top: 0; /* Remove padding to align vertically */
white-space: nowrap;
text-align: right; /* Align the date to the right */
}
.milestone-content {
flex: 1;
background: rgba(255, 255, 255, 0.05);
padding: 1.5rem 2rem;
border-radius: 8px;
margin-left: 2rem;
margin-right: 1rem;
}
.milestone-content h3 {
margin-top: 0;
color: var(--hero-subtitle-background);
}
.milestone-content p {
margin-bottom: 0;
color: var(--body-text);
}
@media (max-width: 768px) {
.roadmap-container {
width: 90%;
padding: 1.6rem;
}
.timeline::before {
left: 50px;
}
.milestone::before {
left: 50px;
}
.milestone-date {
width: 60px;
font-size: 0.9rem;
}
.milestone-content {
margin-left: 1.5rem;
margin-right: 0.5rem;
padding: 1rem 1.5rem;
}
}
@media (max-width: 480px) {
.roadmap-container {
width: 85%;
padding: 1rem;
}
}
</style>

View File

@ -0,0 +1,313 @@
<!-- Signup Modal -->
<div id="signupModal" class="modal">
<div class="modal-content">
<div class="login-box">
<div class="login-header">
<svg class="logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
<h2>Join ThreeFold</h2>
<p>Create your ThreeFold account</p>
</div>
<form id="signup-form" onsubmit="return validateSignupForm(event)">
<div class="form-group compact">
<label for="signup-name">Full Name</label>
<input
type="text"
id="signup-name"
name="name"
required
placeholder="Enter your full name"
autocomplete="name"
>
<span class="error-message" id="signup-nameError"></span>
</div>
<div class="form-group compact">
<label for="signup-email">Email</label>
<input
type="email"
id="signup-email"
name="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
placeholder="Enter your email"
autocomplete="email"
>
<span class="error-message" id="signup-emailError"></span>
</div>
<div class="form-group compact">
<label for="signup-password">Password</label>
<input
type="password"
id="signup-password"
name="password"
required
minlength="8"
placeholder="Enter your password"
autocomplete="new-password"
>
<span class="error-message" id="signup-passwordError"></span>
</div>
<div class="form-group compact">
<label for="signup-tel">Phone Number</label>
<input
type="tel"
id="signup-tel"
name="tel"
required
placeholder="Enter your phone number"
autocomplete="tel"
>
<span class="error-message" id="signup-telError"></span>
</div>
<div class="form-group compact">
<label for="signup-country">Country</label>
<select id="signup-country" name="country" required class="form-control">
<option value="">Select your country</option>
</select>
<span class="error-message" id="signup-countryError"></span>
</div>
<div class="form-group compact">
[[signup_interests]]
<span class="error-message" id="signup-interestsError"></span>
</div>
<button type="submit" class="submit-button">Sign Up</button>
</form>
<div class="signup-link">
Already have an account? <a href="#" onclick="openLoginModal(); closeSignupModal();">Sign in</a>
</div>
<button class="close-button" onclick="closeSignupModal()">&times;</button>
</div>
</div>
</div>
<script>
// Populate countries dropdown
[[system/countries]]
const countrySelect = document.getElementById('signup-country');
countries.forEach(country => {
const option = document.createElement('option');
option.value = country;
option.textContent = country;
countrySelect.appendChild(option);
});
function openSignupModal() {
const modal = document.getElementById('signupModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeSignupModal() {
const modal = document.getElementById('signupModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('signupModal');
if (event.target === modal) {
closeSignupModal();
}
}
// Real-time validation functions
function validatePassword(password) {
return password.length >= 8;
}
function validatePhoneNumber(phone) {
return /^\+?[\d\s-]{10,}$/.test(phone);
}
function validateEmail(email) {
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email);
}
// Real-time validation event listeners
document.getElementById('signup-password')?.addEventListener('input', function(e) {
const passwordError = document.getElementById('signup-passwordError');
if (this.value && !validatePassword(this.value)) {
passwordError.textContent = 'Password must be at least 8 characters long';
} else {
passwordError.textContent = '';
}
});
document.getElementById('signup-tel')?.addEventListener('input', function(e) {
const telError = document.getElementById('signup-telError');
if (this.value && !validatePhoneNumber(this.value)) {
telError.textContent = 'Please enter a valid phone number (min 10 digits)';
} else {
telError.textContent = '';
}
});
document.getElementById('signup-email')?.addEventListener('input', function(e) {
const emailError = document.getElementById('signup-emailError');
if (this.value && !validateEmail(this.value)) {
emailError.textContent = 'Please enter a valid email address';
} else {
emailError.textContent = '';
}
});
function validateSignupForm(event) {
event.preventDefault();
const name = document.getElementById('signup-name');
const email = document.getElementById('signup-email');
const password = document.getElementById('signup-password');
const tel = document.getElementById('signup-tel');
const country = document.getElementById('signup-country');
const interests = document.querySelectorAll('input[name="interests"]:checked');
const nameError = document.getElementById('signup-nameError');
const emailError = document.getElementById('signup-emailError');
const passwordError = document.getElementById('signup-passwordError');
const telError = document.getElementById('signup-telError');
const countryError = document.getElementById('signup-countryError');
const interestsError = document.getElementById('signup-interestsError');
// Reset error messages
nameError.textContent = '';
emailError.textContent = '';
passwordError.textContent = '';
telError.textContent = '';
countryError.textContent = '';
interestsError.textContent = '';
let isValid = true;
// Name validation
if (name.value.trim().length < 2) {
nameError.textContent = 'Please enter your full name';
isValid = false;
}
// Email validation
if (!validateEmail(email.value)) {
emailError.textContent = 'Please enter a valid email address';
isValid = false;
}
// Password validation
if (!validatePassword(password.value)) {
passwordError.textContent = 'Password must be at least 8 characters long';
isValid = false;
}
// Phone validation
if (!validatePhoneNumber(tel.value)) {
telError.textContent = 'Please enter a valid phone number (min 10 digits)';
isValid = false;
}
// Country validation
if (!country.value) {
countryError.textContent = 'Please select your country';
isValid = false;
}
// Interests validation
if (interests.length === 0) {
interestsError.textContent = 'Please select at least one interest';
isValid = false;
}
if (isValid) {
// Here you would typically send the form data to your server
console.log('Form is valid, ready to submit');
closeSignupModal();
}
return false;
}
</script>
<style>
/* Additional styles for signup form */
.form-group.compact {
margin-bottom: 0.5rem;
}
.form-group.compact label {
margin-bottom: 0;
font-size: 0.85rem;
display: block;
line-height: 1.2;
}
.form-group.compact input,
.form-group.compact select {
margin-bottom: 0;
height: 28px;
padding: 0 0.5rem;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin-top: 1rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.8rem;
color: var(--modal-text);
cursor: pointer;
line-height: 1;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
margin: 0.2;
}
select.form-control {
width: 100%;
padding: 0 0.5rem;
border: 1px solid var(--input-border);
border-radius: 4px;
background: var(--body-background);
color: var(--modal-text);
font-size: 0.9rem;
transition: all 0.3s;
height: 35px;
cursor: pointer;
}
select.form-control:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px #1A73E833;
}
.error-message {
color: #dc3545;
font-size: 0.8rem;
margin: 0;
display: block;
min-height: 1rem;
line-height: 1;
}
.modal-content {
padding: 0.8rem;
}
.login-header {
margin-bottom: 1rem;
}
.login-header h2 {
margin-bottom: 0.2rem;
}
</style>

View File

@ -0,0 +1,26 @@
<li class="tf_menu_item">
<a href="../index.html" class="tf_menu_link" @click="mobileMenuOpen = false">Home</a>
</li>
<li class="tf_menu_item">
<a href="#" class="tf_menu_link" @click="mobileMenuOpen = false">Why</a>
<div class="tf_dropdown">
<a href="#" class="tf_dropdown_item">Sustainable</a>
<a href="#" class="tf_dropdown_item">Secure</a>
<a href="#" class="tf_dropdown_item">Scalable</a>
</div>
</li>
<li class="tf_menu_item">
<a href="#" class="tf_menu_link" @click="mobileMenuOpen = false">Products</a>
<div class="tf_dropdown">
<a href="#" class="tf_dropdown_item">Fungistor</a>
<a href="#" class="tf_dropdown_item">ThreeFold</a>
<a href="#" class="tf_dropdown_item">Magic Cloud</a>
</div>
</li>
<li class="tf_menu_item">
<a href="#" class="tf_menu_link" @click="mobileMenuOpen = false">Info</a>
<div class="tf_dropdown">
<a href="/roadmap" class="tf_dropdown_item">Roadmap</a>
</div>
</li>

View File

@ -0,0 +1,35 @@
<label><strong>Interests</strong> (check all which matters)</label>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" name="interests" value="provider">
Become an cloud/internet provider (farmer)
</label>
<label class="checkbox-label">
<input type="checkbox" name="interests" value="vm">
Use cloud capacity for virtual machines
</label>
<label class="checkbox-label">
<input type="checkbox" name="interests" value="ai">
Use cloud capacity for AI
</label>
<label class="checkbox-label">
<input type="checkbox" name="interests" value="blockchain">
Use cloud capacity for Blockchain
</label>
<label class="checkbox-label">
<input type="checkbox" name="interests" value="data">
Use cloud capacity for Data
</label>
<label class="checkbox-label">
<input type="checkbox" name="interests" value="kubernetes">
Use cloud capacity for Kubernetes
</label>
<label class="checkbox-label">
<input type="checkbox" name="interests" value="partner">
Become a preferred Partner.
</label>
<label class="checkbox-label">
<input type="checkbox" name="interests" value="development">
Develop applications (Web4).
</label>
</div>

View File

@ -0,0 +1,24 @@
const countries = [
"Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia",
"Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin",
"Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi",
"Cambodia", "Cameroon", "Canada", "Cape Verde", "Central African Republic", "Chad", "Chile", "China", "Colombia",
"Comoros", "Congo", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica",
"Dominican Republic", "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia",
"Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada",
"Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Honduras", "Hungary", "Iceland", "India", "Indonesia",
"Iran", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya",
"Kiribati", "North Korea", "South Korea", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia",
"Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Macedonia", "Madagascar", "Malawi", "Malaysia", "Maldives",
"Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco",
"Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands",
"New Zealand", "Nicaragua", "Niger", "Nigeria", "Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea",
"Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis",
"Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia",
"Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia",
"South Africa", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Swaziland", "Sweden", "Switzerland",
"Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey",
"Turkmenistan", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay",
"Uzbekistan", "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe"
];

View File

@ -0,0 +1,21 @@
## About Our Platform
ThreeFold provides a revolutionary Internet Infrastructure that enables:
- Decentralized cloud, data & network capacity with reward opportunities for contributors
- Global deployment of web2, web3, AI, edge, and blockchain applications
- Enhanced application access through distributed infrastructure
- Creation of resilient, uncensorable peer-to-peer networks
### Why ThreeFold?
- **First platform in the world to seamlessly combine cloud, data & network capabilities**
- Global Access: Bridging the digital divide for 50% of the world lacking quality Internet infrastructure
- Economic Sovereignty: Enabling countries to build and control their own Internet infrastructure
- Sustainable Growth: Reducing environmental impact through efficient edge computing
### Our Mission & Values
- Planet First: Committed to sustainable and eco-friendly technology solutions
- People Empowerment: Democratizing Internet infrastructure access and ownership
- Digital Sovereignty: Ensuring data privacy and infrastructure independence

View File

@ -0,0 +1,92 @@
<details>
<summary>Is this a separate new Internet?</summary>
<p>
No ThreeFold is a complementary Internet and lives with and on top of
the current Internet. From out of ThreeFold you can still use the
current Internet and interact with it.
</p>
</details>
<details>
<summary>Why do we need a new Internet?</summary>
<p>
The Internet used to be a peer to peer network, but has become fragile
and too centralized. There are so many problems with the current
Internet, such as authenticity, privacy, security, and sustainability
that we believe a fundamental new approach is needed.
</p>
</details>
<details>
<summary>This sounds too big, are you guys are faking it?</summary>
<p>
We have been working on this for over 30 years and ThreeFold is the
result of that work. We have a working product and a growing community
of farmers, users, and partners. We are real and we are here to stay.
</p>
</details>
<details>
<summary>You have 2 tokens, TFT and INCA, why?</summary>
<p>
Thanks to our community there are +60,000 virtual cpu's and +17,000 GB
of storage available on the network.
<a
href="https://dashboard.grid.tf/#/tf-grid/node-statistics/"
target="_blank"
>Checkout our stats on our dashboard.</a
>
TFT is our token which was used to build generation 1,2 and 3 of the
ThreeFold Grid of capacity. TFT is the reward for our loyal community.
There can never be more than 1 billion TFT. We are now building
generation 4 of the ThreeFold Grid of capacity and we need a new token
to build this new generation. There will never be more than 3 billion
INCA. Our partners will start selling new ThreeFold Nodes end Nov 2024
with a new reward scheme and ready to grow to millions of nodes.
</p>
</details>
<details>
<summary>How can I participate?</summary>
<p>
You can participate by becoming a farmer, a user, a partner or by developing a web4 app.
<ul>
<li><a href="/farmer">Provide capacity to the ThreeFold Grid</a></li>
<li><a href="/user">Use capacity off the ThreeFold Grid</a></li>
<li><a href="/builder">Build solutions</a></li>
<li><a href="/hero_coder">Develop applications for Web4</a></li>
</ul>
</p>
</details>
<details>
<summary>What is Web4?</summary>
<p>
Web4 is the next generation of the Internet, where users are in 100% in
control of their data. No centralized services are needed. Blockchain
was the first step to Web3, ThreeFold is the next step to Web4.
ThreeFold is ofcourse fully compatible with Web2 and Web3. In Web4 each
user has a personal Virtual Digital Assistent which is called a Hero.
This hero manages your digital life and on your behalf can interact with
all versions of the Web.
<a href="/hero_intro">Checkout more info about our Hero</a>.
</p>
</details>
<details>
<summary>How secure and private is my data?</summary>
<p>
ThreeFold is designed to be secure and private by default. We use
end-to-end encryption to protect your data and ensure that only you have
access to your data.
</p>
</details>
<details>
<summary>Who should use the ThreeFold Grid. ?</summary>
<p>
Individuals, businesses, and organizations who want to be sovereign and have full control over their data and applications.
Security is a very big problem today, Technology as used by ThreeFold has the potential to resolve this if used properly.
We already work with Governements, NGO's, and Individuals. We are building a channel of solution providers and integrators who want to build on top of ThreeFold.
</p>
</details>

View File

@ -0,0 +1,43 @@
<details>
<summary>What is Web4?</summary>
<p>
Web4 is the next generation of the Internet, where users are in 100% in
control of their data. No centralized services are needed. Blockchain
was the first step to Web3, ThreeFold is the next step to Web4.
ThreeFold is ofcourse fully compatible with Web2 and Web3. In Web4 each
user has a personal Virtual Digital Assistent which is called a Hero.
This hero manages your digital life and on your behalf can interact with
all versions of the Web.
<a href="/hero_intro">Checkout more info about our Hero</a>.
</p>
</details>
<details>
<summary>How secure and private is my data?</summary>
<p>
ThreeFold is designed to be secure and private by default. We use
end-to-end encryption to protect your data and ensure that only you have
access to your data.
</p>
</details>
<details>
<summary>How secure and private is my data?</summary>
<p>
ThreeFold is designed to be secure and private by default. We use
end-to-end encryption to protect your data and ensure that only you have
access to your data.
</p>
</details>
<details>
<summary>Who should care bout Web4. ?</summary>
<p>
While blockchain is a very good first step, we believe its time to move
to the next level. Web4 is the next generation of the Internet, where
users are in 100% in control of their data. This hero manages your
digital life and on your behalf can interact with all versions of the
Web. Enterprises, Governments, Software Developers and Individuals
should care about Web4.
</p>
</details>

23
poc_threefold/index.html Normal file
View File

@ -0,0 +1,23 @@
{% include 'components/header.html' %}
<body>
{% include 'components/nav.html' %}
<main>
{% include 'components/globe.html' %}
{% set config = {
"title": "THREEFOLD AUTONOMOUS INTERNET.",
"subtitle": "Building a decentralized internet.",
"image": "static/img/wave.png" }
%}
{% include 'components/hero_image.html' %}
{% set config = { "name": "threefold/faq_tf", "section_name": "threefold/faq_tf_intro" } %}
{% include 'components/faq.html' %}
</main>
{% include 'components/footer.html' %}
{% include 'components/login.html' %}
{% include 'components/signup.html' %}
</body>
</html>

View File

@ -0,0 +1,283 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - ThreeFold</title>
<link rel="stylesheet" href="components/nav.html">
</head>
<body>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<svg class="logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
<h1>Welcome Back</h1>
<p>Sign in to your ThreeFold account</p>
</div>
<form id="loginForm" onsubmit="return validateForm(event)">
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
placeholder="Enter your email"
autocomplete="email"
>
<span class="error-message" id="emailError"></span>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
required
minlength="8"
placeholder="Enter your password"
autocomplete="current-password"
>
<span class="error-message" id="passwordError"></span>
</div>
<div class="form-footer">
<label class="remember-me">
<input type="checkbox" name="remember"> Remember me
</label>
<a href="#" class="forgot-password">Forgot Password?</a>
</div>
<button type="submit" class="login-button">Sign In</button>
</form>
<div class="signup-link">
Don't have an account? <a href="#">Sign up</a>
</div>
</div>
</div>
<style>
:root {
--primary-color: #1a73e8;
--error-color: #dc3545;
--text-color: #333;
--border-color: #ddd;
--background-color: #f5f5f5;
--box-shadow: 0 2px 10px #0000001A;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--background-color);
color: var(--text-color);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
width: 100%;
max-width: 400px;
padding: 10px;
}
.login-box {
background: white;
border-radius: 8px;
box-shadow: var(--box-shadow);
padding: 2rem;
}
.login-header {
text-align: center;
margin-bottom: 1.5rem;
}
.logo {
width: 48px;
height: 48px;
margin-bottom: 1rem;
color: var(--primary-color);
}
.login-header h1 {
margin: 0;
font-size: 1.75rem;
color: var(--text-color);
}
.login-header p {
margin: 0.5rem 0 0;
color: #666;
font-size: 0.95rem;
}
.form-group {
margin-bottom: 0.5rem; /* Reduced from 1rem */
}
label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-color);
font-size: 0.9rem;
font-weight: 500;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s, box-shadow 0.3s;
box-sizing: border-box;
}
input[type="email"]:focus,
input[type="password"]:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px #1A73E833;
}
.error-message {
display: block;
color: var(--error-color);
font-size: 0.85rem;
margin-top: 0.25rem;
min-height: 1.25em;
}
.form-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem; /* Reduced from 1rem */
}
.remember-me {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #666;
}
.forgot-password {
font-size: 0.9rem;
color: var(--primary-color);
text-decoration: none;
}
.forgot-password:hover {
text-decoration: underline;
}
.login-button {
width: 100%;
padding: 0.75rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s, transform 0.3s;
}
.login-button:hover {
background: #1557b0;
transform: translateY(-1px);
}
.login-button:active {
transform: translateY(0);
}
.signup-link {
text-align: center;
margin-top: 1rem;
font-size: 0.9rem;
color: #666;
}
.signup-link a {
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
}
.signup-link a:hover {
text-decoration: underline;
}
@media (max-width: 480px) {
.login-container {
padding: 10px;
}
.login-box {
padding: 1.5rem;
}
}
</style>
<script>
function validateForm(event) {
event.preventDefault();
const email = document.getElementById('email');
const emailError = document.getElementById('emailError');
const password = document.getElementById('password');
const passwordError = document.getElementById('passwordError');
// Reset error messages
emailError.textContent = '';
passwordError.textContent = '';
let isValid = true;
// Email validation
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email.value)) {
emailError.textContent = 'Please enter a valid email address';
isValid = false;
}
// Password validation
if (password.value.length < 8) {
passwordError.textContent = 'Password must be at least 8 characters long';
isValid = false;
}
if (isValid) {
// Here you would typically send the form data to your server
console.log('Form is valid, ready to submit');
// For demo purposes, we'll just log the email
console.log('Email:', email.value);
}
return isValid;
}
// Real-time email validation
document.getElementById('email').addEventListener('input', function(e) {
const emailError = document.getElementById('emailError');
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (this.value && !emailRegex.test(this.value)) {
emailError.textContent = 'Please enter a valid email address';
} else {
emailError.textContent = '';
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,299 @@
{% include 'components/header.html' %}
<body>
{% include 'components/nav.html' %}
<main>
<div class="container">
<h1>Identity Test Page</h1>
<div class="card">
<div id="status" class="status unauthenticated">
Not identified
</div>
<div class="actions">
<button class="btn-identify" onclick="showIdentifyModal()">Identify</button>
<button class="btn-clear" onclick="clearAllAndReload()">Clear All & Reload</button>
</div>
<div id="keyInfo" class="key-info" style="display: none;">
<h3>Key Information</h3>
<div class="key-details">
<div id="keyStatus"></div>
<div id="publicKey" class="public-key"></div>
<div id="generateKeySection" style="display: none;" class="generate-key-section">
<div class="warning-message">
⚠️ No private key found. You need to generate a private key to use the system.
<strong>WARNING: Losing your private key will result in permanent loss of access to your digital life in ThreeFold.</strong>
</div>
<button onclick="generatePrivateKey()" class="btn-generate">Generate Private Key</button>
</div>
</div>
</div>
<div id="testActions" class="test-actions" style="display: none;">
<h3>Test Actions</h3>
<div class="test-container">
<div class="test-group">
<input type="text" id="testKey" placeholder="Key name">
<input type="text" id="testValue" placeholder="Value">
<button onclick="setTestValue()">Set Value</button>
<div id="setResult" class="set-result"></div>
</div>
<div class="test-group">
<input type="text" id="retrieveKey" placeholder="Key to retrieve">
<button onclick="getTestValue()">Get Value</button>
<div id="retrievedValue" class="retrieved-value"></div>
</div>
<div class="test-group">
<button onclick="listAllValues()" class="btn-list">List All Values</button>
<div id="allValues" class="all-values"></div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Identity Modal -->
<div class="modal-overlay" id="identifyModal">
<div class="modal-container">
<div class="modal-header">
<h2>Identify Yourself</h2>
<p class="modal-description">Enter your secret password to access the key management system.</p>
</div>
<form class="modal-form" id="identifyForm" onsubmit="return handleIdentify(event)">
<div class="form-group">
<label for="secret">Secret Password</label>
<input type="password" id="secret" name="secret" required
placeholder="Enter your secret password"
autocomplete="current-password">
<div class="error-message" id="errorMessage"></div>
</div>
<div class="modal-buttons">
<button type="button" class="btn btn-secondary" onclick="closeIdentifyModal()">Cancel</button>
<button type="submit" class="btn btn-primary" id="identifyButton">Identify</button>
</div>
</form>
</div>
</div>
<script>
function showIdentifyModal() {
document.getElementById('identifyModal').style.display = 'block';
document.getElementById('secret').focus();
}
function closeIdentifyModal() {
document.getElementById('identifyModal').style.display = 'none';
document.getElementById('identifyForm').reset();
document.getElementById('errorMessage').style.display = 'none';
}
function clearAllAndReload() {
sessionStorage.clear();
location.reload();
}
async function generatePrivateKey() {
try {
const privateKey = await window.kvs.generateNewPrivateKey();
const publicKey = window.kvs.getPublicKey(privateKey);
const publicKeyBase58 = bs58.encode(publicKey);
document.getElementById('generateKeySection').style.display = 'none';
document.getElementById('keyStatus').textContent = '🔑 New private key generated';
document.getElementById('publicKey').innerHTML =
`<strong>Public Key (Base58):</strong><br>${publicKeyBase58}`;
} catch (error) {
console.error('Error generating private key:', error);
document.getElementById('keyStatus').textContent =
'❌ Error generating private key: ' + error.message;
}
}
async function updateStatus() {
const status = document.getElementById('status');
const keyInfo = document.getElementById('keyInfo');
const testActions = document.getElementById('testActions');
const keyStatus = document.getElementById('keyStatus');
const generateKeySection = document.getElementById('generateKeySection');
const publicKeyDiv = document.getElementById('publicKey');
if (window.kvs.isSessionValid()) {
try {
status.className = 'status authenticated';
status.textContent = 'Identified successfully';
keyInfo.style.display = 'block';
testActions.style.display = 'block';
const hasKey = await window.kvs.hasPrivateKey();
if (!hasKey) {
generateKeySection.style.display = 'block';
keyStatus.textContent = '⚠️ No private key found';
publicKeyDiv.innerHTML = '';
testActions.style.display = 'none';
} else {
generateKeySection.style.display = 'none';
const privateKey = await window.kvs.getPrivateKey();
const publicKey = window.kvs.getPublicKey(privateKey);
const publicKeyBase58 = bs58.encode(publicKey);
keyStatus.textContent = '🔑 Private key loaded';
publicKeyDiv.innerHTML = `<strong>Public Key (Base58):</strong><br>${publicKeyBase58}`;
}
} catch (error) {
status.className = 'status unauthenticated';
status.textContent = 'Error accessing keys: ' + error.message;
keyInfo.style.display = 'none';
testActions.style.display = 'none';
}
} else {
status.className = 'status unauthenticated';
status.textContent = 'Not identified';
keyInfo.style.display = 'none';
testActions.style.display = 'none';
showIdentifyModal();
}
}
async function handleIdentify(event) {
event.preventDefault();
const secret = document.getElementById('secret').value;
const errorMessage = document.getElementById('errorMessage');
const identifyButton = document.getElementById('identifyButton');
// Disable button during processing
identifyButton.disabled = true;
identifyButton.textContent = 'Processing...';
try {
if (!window.kvs) {
throw new Error('Key management system not initialized');
}
// Set the password in session storage
window.kvs.setPassword(secret);
// If successful, close the modal
closeIdentifyModal();
// Dispatch an event to notify that identification was successful
window.dispatchEvent(new CustomEvent('userIdentified'));
} catch (error) {
console.error('Identification error:', error);
errorMessage.textContent = error.message || 'Invalid secret password';
errorMessage.style.display = 'block';
window.kvs.clearSession();
} finally {
// Re-enable button
identifyButton.disabled = false;
identifyButton.textContent = 'Identify';
}
return false;
}
async function setTestValue() {
const key = document.getElementById('testKey').value;
const value = document.getElementById('testValue').value;
const setResult = document.getElementById('setResult');
const retrieveKey = document.getElementById('retrieveKey');
if (!key || !value) {
setResult.innerHTML = '<div class="error">Please enter both key and value</div>';
return;
}
try {
await window.kvs.set(key, value);
setResult.innerHTML = '<div class="success">Value set successfully!</div>';
document.getElementById('testKey').value = '';
document.getElementById('testValue').value = '';
// Auto-fill the retrieve key field
retrieveKey.value = key;
// Automatically get the value
await getTestValue();
// Refresh the list if it's visible
const allValues = document.getElementById('allValues');
if (allValues.innerHTML !== '') {
await listAllValues();
}
} catch (error) {
setResult.innerHTML = `<div class="error">Error setting value: ${error.message}</div>`;
}
}
async function getTestValue() {
const key = document.getElementById('retrieveKey').value;
const retrievedValueDiv = document.getElementById('retrievedValue');
if (!key) {
retrievedValueDiv.innerHTML = '<div class="error">Please enter a key to retrieve</div>';
return;
}
try {
const value = await window.kvs.get(key);
retrievedValueDiv.innerHTML = value ?
`<div class="success">Retrieved value: ${value}</div>` :
'<div class="info">No value found for this key</div>';
} catch (error) {
retrievedValueDiv.innerHTML = `<div class="error">Error: ${error.message}</div>`;
}
}
async function listAllValues() {
const allValuesDiv = document.getElementById('allValues');
try {
const results = await window.kvs.listAll();
if (results.length === 0) {
allValuesDiv.innerHTML = '<div class="no-values">No stored values found</div>';
return;
}
const html = results.map(({key, value, encrypted}) => `
<div class="stored-value ${encrypted ? 'encrypted' : 'not-encrypted'}">
<div class="stored-key">${key}</div>
<div class="stored-value-content">${value}</div>
<div class="encryption-status">
${encrypted ?
'<span class="status-icon">🔒</span> Encrypted' :
'<span class="status-icon">⚠️</span> Not encrypted'}
</div>
</div>
`).join('');
allValuesDiv.innerHTML = html;
} catch (error) {
allValuesDiv.innerHTML = `<div class="error">Error listing values: ${error.message}</div>`;
}
}
// Check session validity periodically
const sessionCheckInterval = setInterval(() => {
if (window.kvs && !window.kvs.isSessionValid()) {
window.kvs.clearSession();
updateStatus();
}
}, 60000); // Check every minute
// Cleanup interval when page unloads
window.addEventListener('unload', () => {
clearInterval(sessionCheckInterval);
});
// Initial check when the page loads
window.addEventListener('DOMContentLoaded', () => {
updateStatus();
});
// Update status when user is identified
window.addEventListener('userIdentified', updateStatus);
</script>
</body>
</html>

9
poc_threefold/page1.html Normal file
View File

@ -0,0 +1,9 @@
{% include 'components/header.html' %}
<body>
{% include 'components/nav.html' %}
<main>
{% include 'components/hero1.html' %}
</main>
{% include 'components/footer.html' %}
</body>
</html>

10
poc_threefold/page2.html Normal file
View File

@ -0,0 +1,10 @@
{% include 'components/header.html' %}
<body>
{% include 'components/nav.html' %}
<main>
{% include 'components/faq.html' %}
</main>
{% include 'components/footer.html' %}
</body>
</html>

View File

@ -0,0 +1 @@
{{/* <script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script> */}}

View File

@ -0,0 +1,21 @@
{% include 'components/header.html' %}
<body>
{% include 'components/nav.html' %}
<main>
{% set config = {
"title": "ThreeFold INTERNET ROADMAP",
"subtitle": "Building a decentralized internet, for a better world",
"image": "static/img/questions.png" }
%}
{% include 'components/hero_image.html' %}
{% set config_roadmap = { "name": "threefold/faq_tf", "section_name": "threefold/faq_tf_section" } %}
{% include 'components/roadmap.html' %}
</main>
{% include 'components/footer.html' %}
</body>
</html>

189
poc_threefold/server.py Normal file
View File

@ -0,0 +1,189 @@
from fastapi import FastAPI, Request, HTTPException, WebSocket
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from jinja2 import Environment, FileSystemLoader, select_autoescape, TemplateNotFound
import os
import markdown
import re
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import asyncio
from typing import List
import time
from contextlib import asynccontextmanager
# Get BASE_DIR from environment variables with a default fallback
BASE_DIR = os.environ.get('BASE_DIR', os.path.dirname(os.path.abspath(__file__)))
# Check if BASE_DIR exists
if not os.path.exists(BASE_DIR):
raise RuntimeError(f"The BASE_DIR '{BASE_DIR}' does not exist.")
sources_dir = os.path.expanduser(f"{BASE_DIR}/poc_threefold")
if not os.path.exists(sources_dir):
raise RuntimeError(f"The source directory '{sources_dir}' does not exist.")
static_dir = f"{sources_dir}/static"
content_dir = f"{sources_dir}/content"
# Store active WebSocket connections
active_connections: List[WebSocket] = []
# Store the main event loop reference
main_loop = None
class FileChangeHandler(FileSystemEventHandler):
def __init__(self):
self.last_modified = 0
def on_modified(self, event):
if event.is_directory:
return
current_time = time.time()
if current_time - self.last_modified > 0.5: # Debounce multiple events
self.last_modified = current_time
if main_loop is not None:
asyncio.run_coroutine_threadsafe(broadcast_reload(), main_loop)
async def broadcast_reload():
for connection in active_connections[:]: # Create a copy of the list to iterate over
try:
await connection.send_text("reload")
except:
if connection in active_connections:
active_connections.remove(connection)
def get_content(name: str) -> str:
"""Get content by name from either HTML or markdown files in content directory"""
# Remove any leading/trailing slashes
name = name.strip('/')
# Check for file with .html extension
html_path = os.path.join(content_dir, f"{name}.html")
if os.path.exists(html_path):
with open(html_path, 'r') as f:
return f.read()
# Check for file with .md extension
md_path = os.path.join(content_dir, f"{name}.md")
if os.path.exists(md_path):
with open(md_path, 'r') as f:
content = f.read()
return markdown.markdown(content)
return f"[[{name} not found]]"
def process_content(content: str) -> str:
"""Process content and replace [[name]] with corresponding HTML content"""
def replace_content(match):
name = match.group(1)
return get_content(name)
# Replace all [[name]] patterns
content = re.sub(r'\[\[(.*?)\]\]', replace_content, content)
# Inject live reload script before </body>
reload_script = """
<script>
const ws = new WebSocket('ws://' + window.location.host + '/ws');
ws.onmessage = function(event) {
if (event.data === 'reload') {
window.location.reload();
}
};
ws.onclose = function() {
console.log('WebSocket connection closed. Retrying...');
setTimeout(() => {
window.location.reload();
}, 1000);
};
</script>
"""
content = content.replace('</body>', f'{reload_script}</body>')
return content
# Setup file watcher and store observer reference
observer = None
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
global main_loop, observer
main_loop = asyncio.get_running_loop()
# Setup file watcher
event_handler = FileChangeHandler()
observer = Observer()
observer.schedule(event_handler, sources_dir, recursive=True)
observer.start()
yield
# Shutdown
if observer:
observer.stop()
observer.join()
app = FastAPI(lifespan=lifespan)
if not os.path.exists(static_dir):
raise RuntimeError(f"The directory '{static_dir}' does not exist.")
if not os.path.exists(sources_dir):
raise RuntimeError(f"The templates dir '{sources_dir}' does not exist.")
# Mount the static files directory
app.mount("/static", StaticFiles(directory=static_dir), name="static")
env = Environment(
loader=FileSystemLoader(sources_dir),
autoescape=select_autoescape(['html', 'xml'])
)
# Initialize Jinja2 templates
templates = Jinja2Templates(directory=sources_dir)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
active_connections.append(websocket)
try:
while True:
await websocket.receive_text()
except:
if websocket in active_connections:
active_connections.remove(websocket)
@app.get("/favicon.ico")
async def favicon():
# First try to serve from static directory
favicon_path = os.path.join(static_dir, "favicon.ico")
if os.path.exists(favicon_path):
return FileResponse(favicon_path)
# If not found, return 404
raise HTTPException(status_code=404, detail="Favicon not found")
@app.get("/", response_class=HTMLResponse)
async def read_index(request: Request):
template = env.get_template("index.html")
content = template.render(request=request)
return process_content(content)
@app.get("/{path:path}", response_class=HTMLResponse)
async def read_template(request: Request, path: str):
# Add .html extension if not present
if not path.endswith('.html'):
path = f"{path}.html"
try:
# Try to load and render the template (this will work for both direct files and templates)
template = env.get_template(path)
content = template.render(request=request)
return process_content(content)
except TemplateNotFound:
raise HTTPException(status_code=404, detail=f"Template {path} not found")
if __name__ == "__main__":
import uvicorn
uvicorn.run("server:app", host="127.0.0.1", port=8001, reload=True)

View File

@ -0,0 +1,171 @@
/* Import Google Fonts - if not already imported in ourworld.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
/* Container styling */
.faq-container {
display: flex;
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Markdown content styling */
.markdown-content {
flex: 1;
padding-right: 2rem;
border-right: 1px solid #a4b6ba;
}
/* Base styles for markdown content */
.markdown-content {
font-family: 'Inter';
font-weight: 200;
}
/* Heading styles */
.markdown-content h2,
.markdown-content h3 {
font-family: 'Inter';
}
/* Light theme colors */
.light-theme .markdown-content h2,
.light-theme .markdown-content h3,
.light-theme .markdown-content p,
.light-theme .markdown-content ul,
.light-theme .markdown-content li {
color: #444444 !important;
}
/* Dark theme colors */
.markdown-content h2,
.markdown-content h3,
.markdown-content p,
.markdown-content ul,
.markdown-content li {
color: #cccccc !important;
}
.markdown-content h2 {
font-size: 1.2rem;
font-weight: 500;
line-height: 1.2;
margin-bottom: 1rem;
}
.markdown-content h3 {
font-size: 1rem;
font-weight: 500;
line-height: 1.3;
margin-bottom: 0.8rem;
}
/* Regular text styles */
.markdown-content p,
.markdown-content ul,
.markdown-content li {
font-family: 'Inter', sans-serif;
font-weight: 400;
}
/* Make list items and paragraphs consistent size */
.markdown-content p,
.markdown-content li {
font-size: 0.75rem;
font-weight: 200;
line-height: 1.25;
margin-bottom: 0.25rem;
padding-left: 1rem; /* Added indentation for paragraphs */
}
/* List specific styling */
.markdown-content ul {
font-weight: 200;
padding-left: 2.5rem; /* Increased padding for better alignment */
margin-bottom: 1rem;
}
/* FAQ section styling */
.faq-section {
flex: 1;
}
details {
border-bottom: 1px solid #ddd;
padding: 1em 0;
margin: 0;
}
summary {
font-family: 'Inter', sans-serif;
font-size: 0.8rem;
font-weight: 200;
cursor: pointer;
list-style: none;
margin: 0;
padding: 0;
outline: none;
}
.faq-section details summary {
color: #96989ead; /* ThreeFold blue color */
}
.faq-section details summary:before {
color: #2a2a2c; /* Makes the arrow icon match */
}
summary::marker {
display: none;
}
details[open] summary {
color: #000;
}
details p {
font-family: 'Inter', sans-serif;
margin: 0.5em 0 0 0;
color: #666;
font-size: 0.8rem;
line-height: 1.6;
font-weight: 400;
padding-left: 1.5rem; /* Added indentation for FAQ answers */
}
/* Links styling */
.markdown-content a,
details a {
font-size: 0.75rem; /* Match paragraph font size */
font-family: 'Inter', sans-serif;
font-weight: 200;
text-decoration: none;
}
summary:before {
content: "▶";
display: inline-block;
transform: rotate(0deg);
transition: transform 0.3s ease;
margin-right: 0.5em;
color: #000;
}
details[open] summary:before {
transform: rotate(90deg);
}
/* Responsive design */
@media (max-width: 768px) {
.faq-container {
flex-direction: column;
}
.markdown-content {
border-right: none;
border-bottom: 1px solid #ddd;
padding-right: 0;
padding-bottom: 2rem;
}
}

View File

@ -0,0 +1,178 @@
/* Modal Styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #00000080;
z-index: 1001;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--modal-background);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px #00000033;
position: relative;
width: 100%;
max-width: 460px;
color: var(--modal-text);
}
.login-box {
position: relative;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.logo {
width: 48px;
height: 48px;
margin-bottom: 1rem;
color: var(--text-color);
}
.login-header h2 {
margin: 0;
color: var(--modal-text);
font-size: 1.3rem;
}
.login-header p {
margin: 0.5rem 0 0;
color: var(--hover-color);
font-size: 0.85rem;
}
.form-group {
margin-bottom: 0.3rem;
}
.form-group label {
display: block;
margin-bottom: 0.3rem;
color: var(--modal-text);
font-size: 0.8rem;
}
.form-group input {
width: 100%;
padding: 0.25rem 0.5rem;
border: 1px solid var(--input-border);
border-radius: 4px;
background: var(--body-background);
color: var(--modal-text);
font-size: 0.9rem;
transition: all 0.3s;
height: 40px;
}
.form-group input:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px #1A73E833;
}
.error-message {
color: #dc3545;
font-size: 0.75rem;
margin-top: 0.25rem;
display: block;
min-height: 1.25em;
}
.form-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.remember-me {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--modal-text);
font-size: 0.8rem;
}
.forgot-password {
color: #1a73e8;
text-decoration: none;
font-size: 0.8rem;
}
.forgot-password:hover {
text-decoration: underline;
}
.submit-button {
width: 100%;
padding: 0.75rem;
background: #1a73e8;
color: white;
border: none;
border-radius: 4px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.submit-button:hover {
background: #1557b0;
transform: translateY(-1px);
}
.signup-link {
text-align: center;
margin-top: 1.5rem;
color: var(--modal-text);
font-size: 0.8rem;
}
.signup-link a {
color: #1a73e8;
text-decoration: none;
font-weight: 500;
}
.signup-link a:hover {
text-decoration: underline;
}
.close-button {
position: absolute;
top: -1rem;
right: -1rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
border: none;
background: var(--modal-text);
color: var(--modal-background);
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.close-button:hover {
transform: scale(1.1);
}
@media (max-width: 768px) {
.modal-content {
margin: 1rem;
padding: 1.5rem;
}
}

View File

@ -0,0 +1,367 @@
<style>
.container {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.card {
background: white;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.status {
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
text-align: center;
font-weight: bold;
}
.status.authenticated {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.unauthenticated {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.actions {
display: flex;
gap: 1rem;
margin: 1rem 0;
justify-content: center;
}
.btn-identify, .btn-clear, .btn-list, .btn-generate {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s;
}
.btn-identify {
background-color: #007bff;
color: white;
}
.btn-identify:hover {
background-color: #0056b3;
}
.btn-clear {
background-color: #dc3545;
color: white;
}
.btn-clear:hover {
background-color: #c82333;
}
.btn-list {
background-color: #17a2b8;
color: white;
width: 100%;
}
.btn-list:hover {
background-color: #138496;
}
.btn-generate {
background-color: #28a745;
color: white;
margin-top: 1rem;
width: 100%;
}
.btn-generate:hover {
background-color: #218838;
}
.key-info {
background-color: #f8f9fa;
padding: 1.5rem;
border-radius: 4px;
margin: 1.5rem 0;
color: #2c3e50;
}
.key-details {
margin-top: 1rem;
}
.warning-message {
background-color: #fff3cd;
color: #856404;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
border: 1px solid #ffeeba;
}
.public-key {
word-break: break-all;
font-family: monospace;
background: #ffffff;
padding: 1rem;
border-radius: 4px;
border: 1px solid #dee2e6;
color: #2c3e50;
margin-top: 1rem;
}
.test-actions {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #dee2e6;
color: #2c3e50;
}
.test-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.test-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.test-group input {
padding: 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
color: #2c3e50;
background: #ffffff;
}
.test-group button {
padding: 0.5rem;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.test-group button:hover {
background-color: #218838;
}
.set-result, .retrieved-value {
margin-top: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
font-family: monospace;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
padding: 0.5rem;
border-radius: 4px;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
padding: 0.5rem;
border-radius: 4px;
}
.info {
background-color: #e2e3e5;
color: #383d41;
border: 1px solid #d6d8db;
padding: 0.5rem;
border-radius: 4px;
}
.all-values {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.stored-value {
background: #ffffff;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 1rem;
}
.stored-key {
font-weight: bold;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.stored-value-content {
font-family: monospace;
background: #f8f9fa;
padding: 0.5rem;
border-radius: 4px;
margin: 0.5rem 0;
word-break: break-all;
}
.encryption-status {
font-size: 0.875rem;
color: #6c757d;
}
.status-icon {
margin-right: 0.5rem;
}
.encrypted .encryption-status {
color: #28a745;
}
.not-encrypted .encryption-status {
color: #dc3545;
}
.no-values {
text-align: center;
color: #6c757d;
padding: 1rem;
background: #f8f9fa;
border-radius: 4px;
}
/* Modal Styles */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
backdrop-filter: blur(3px);
}
.modal-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 1001;
width: 90%;
max-width: 400px;
}
.modal-header {
margin-bottom: 1.5rem;
text-align: center;
}
.modal-description {
color: #666;
margin-top: 0.5rem;
font-size: 0.9rem;
}
.modal-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: bold;
color: #333;
}
.form-group input {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1.5rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
font-weight: 500;
}
.btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background-color: #545b62;
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.5rem;
display: none;
padding: 0.5rem;
background-color: #fff5f5;
border-radius: 4px;
border: 1px solid #ffebee;
}
</style>

View File

@ -0,0 +1,272 @@
.tf_nav {
background: var(--body-background);
padding: 0.3rem;
position: sticky;
top: 0;
z-index: 1000;
box-shadow: 0 1px 4px #00000022;
color: var(--text-color);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.tf_nav_container {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
height: 32px;
}
.tf_logo {
display: flex;
align-items: center;
gap: 0.5rem;
margin-right: 4rem;
height: 100%;
color: var(--text-color);
text-decoration: none;
font-size: 0.95rem;
}
.tf_logo svg {
width: 20px;
height: 20px;
}
.tf_menu {
display: flex;
align-items: center;
gap: 0.75rem;
list-style: none;
margin: 0;
padding: 0;
height: 100%;
margin-right: auto;
}
.tf_menu_item {
position: relative;
height: 100%;
display: flex;
align-items: center;
}
.tf_menu_link {
color: var(--text-color);
text-decoration: none;
padding: 0.3rem;
display: flex;
align-items: center;
height: 100%;
transition: color 0.2s;
font-size: 0.8rem;
font-weight: 450;
}
.tf_menu_link:hover {
color: var(--hover-color);
}
.tf_dropdown {
position: absolute;
top: 100%;
left: 0;
background: var(--body-background);
min-width: 150px;
border-radius: 4px;
box-shadow: 0 4px 12px #00000033;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s;
}
.tf_menu_item:hover .tf_dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.tf_dropdown_item {
padding: 0.4rem 0.8rem;
color: var(--text-color);
text-decoration: none;
display: block;
transition: background-color 0.2s;
font-size: 0.6rem;
}
.tf_dropdown_item:hover {
background-color: #80808019;
}
.tf_right_controls {
display: flex;
align-items: center;
gap: 2rem;
margin-left: 2rem;
}
.tf_theme_toggle {
background: none;
border: 1px solid var(--text-color);
border-radius: 50%;
cursor: pointer;
padding: 0.3rem;
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
width: 28px;
height: 28px;
}
.tf_theme_toggle:hover {
background-color: #80808019;
transform: scale(1.05);
}
.theme-icon {
width: 14px;
height: 14px;
transition: all 0.2s;
}
.light-theme .theme-icon.light {
display: none;
}
.light-theme .theme-icon.dark {
display: block;
}
.theme-icon.light {
display: block;
}
.theme-icon.dark {
display: none;
}
.tf_login_btn {
background: transparent;
border: 1px solid var(--text-color);
color: var(--text-color);
padding: 0.25rem 1rem;
border-radius: 4px;
text-decoration: none;
transition: all 0.2s;
font-weight: 500;
font-size: 0.8125rem;
cursor: pointer;
}
.tf_login_btn:hover {
background: var(--text-color);
color: var(--body-background);
transform: translateY(-1px);
box-shadow: 0 2px 4px #0000001A;
}
.hamburger-menu {
display: none;
cursor: pointer;
border: none;
background: none;
padding: 0.5rem;
color: var(--text-color);
}
.hamburger-menu svg {
width: 24px;
height: 24px;
color: var(--text-color);
}
.mobile-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
transition: opacity 0.3s ease;
}
.mobile-overlay.active {
display: block;
opacity: 1;
}
@media (max-width: 768px) {
.tf_menu {
display: none;
position: fixed;
top: 48px;
left: 0;
right: 0;
background: var(--body-background);
flex-direction: column;
align-items: stretch;
padding: 1rem;
height: auto;
box-shadow: 0 4px 12px #00000033;
z-index: 1000;
max-height: calc(100vh - 48px);
overflow-y: auto;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.tf_menu.active {
display: flex;
transform: translateY(0);
}
.tf_menu_item {
height: auto;
flex-direction: column;
align-items: stretch;
}
.tf_menu_link {
padding: 0.8rem;
font-size: 1rem;
}
.tf_dropdown {
position: static;
opacity: 1;
visibility: visible;
transform: none;
box-shadow: none;
background: transparent;
margin: 0;
padding-left: 1rem;
}
.tf_dropdown_item {
font-size: 0.9rem;
padding: 0.6rem 1rem;
}
.tf_logo {
margin-right: 0;
}
.hamburger-menu {
display: block;
order: -1;
}
.tf_right_controls {
margin-left: auto;
gap: 1rem;
}
}

View File

@ -0,0 +1,187 @@
/* Import Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
/* Light theme variables */
--body-background-light: #ffffff;
--body-text-light: #333333;
--text-color-light: #333;
--hover-color-light: #666;
--modal-background-light: #f5f5f5;
--modal-text-light: #333;
--input-border-light: #ddd;
--hero-background-light: #FFFFFFF2;
--hero-background2-light: #FFFFFFFB;
--hero-text-light: #333;
--hero-banner-background-light: #0000000D;
--hero-subtitle-background-light: #385bb5;
--hero-subtitle-text-light: white;
--input-border-light: var(--hero-background2-light);
/* Dark theme variables */
--body-background-dark: #1a1a1a;
--body-text-dark: #ffffff;
--text-color-dark: #fff;
--hover-color-dark: #aaa;
--modal-background-dark: #282c34;
--modal-text-dark: #fff;
--input-border-dark: #444;
--hero-background-dark: #282C34F2;
--hero-background2-dark: #2D3139FA;
--hero-text-dark: white;
--hero-banner-background-dark: #FFFFFF26;
--hero-subtitle-background-dark: #385bb5;
--hero-subtitle-text-dark: white;
--input-border-dark: var(--hero-background2-dark);
/* Default to dark theme */
--body-background: var(--body-background-dark);
--body-text: var(--body-text-dark);
--text-color: var(--text-color-dark);
--hover-color: var(--hover-color-dark);
--modal-background: var(--modal-background-dark);
--modal-text: var(--modal-text-dark);
--input-border: var(--input-border-dark);
--hero-background: var(--hero-background-dark);
--hero-background2: var(--hero-background2-dark);
--hero-text: var(--hero-text-dark);
--hero-banner-background: var(--hero-banner-background-dark);
--hero-subtitle-background: var(--hero-subtitle-background-dark);
--hero-subtitle-text: var(--hero-subtitle-text-dark);
--input-border: var(--input-border-dark);
}
/* Light theme class */
.light-theme {
--body-background: var(--body-background-light);
--body-text: var(--body-text-light);
--text-color: var(--text-color-light);
--hover-color: var(--hover-color-light);
--modal-background: var(--modal-background-light);
--modal-text: var(--modal-text-light);
--input-border: var(--input-border-light);
--hero-background: var(--hero-background-light);
--hero-background2: var(--hero-background2-light);
--hero-text: var(--hero-text-light);
--hero-banner-background: var(--hero-banner-background-light);
--hero-subtitle-background: var(--hero-subtitle-background-light);
--hero-subtitle-text: var(--hero-subtitle-text-light);
--input-border: var(--input-border-light);
}
/* Heading styles - using Inter */
h1 {
font-family: 'Inter', sans-serif;
font-size: 1.2rem;
margin-bottom: 1rem;
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
text-transform: uppercase;
}
h2 {
font-family: 'Inter', sans-serif;
font-size: 1.1rem;
margin-bottom: 1rem;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.2;
}
h3 {
font-family: 'Inter', sans-serif;
font-size: 1rem;
margin-bottom: 0.8rem;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.3;
}
h4 {
font-family: 'Inter', sans-serif;
font-size: 1rem;
margin-bottom: 0.8rem;
font-weight: 500;
letter-spacing: -0.005em;
line-height: 1.4;
}
p ul {
padding-left: 1.5em;
margin: 1.25em 0;
}
p li {
line-height: 1.6;
margin-bottom: 0.75em;
}
/* Paragraph styles - using Inter */
p {
font-family: 'Inter', sans-serif;
font-size: 1rem;
line-height: 1.6;
margin-bottom: 1.5rem;
max-width: 720px;
font-weight: 200;
}
/* Optional: styling for small or additional text */
small {
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
font-weight: 200;
color: #666;
}
/* Additional styles for links within text */
a {
color: #007acc;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
a:hover {
text-decoration: underline;
}
/* Navigation styles - using Inter */
nav {
font-family: 'Inter';
}
nav a {
font-size: 1rem;
font-weight: 500;
letter-spacing: 0;
text-decoration: none;
color: var(--text-color);
transition: color 0.2s ease;
text-transform: uppercase;
}
nav a:hover {
color: var(--hover-color);
}
main {
background-color: var(--body-background);
transition: background-color 0.3s;
}
body {
font-family: 'Inter';
background-color: var(--body-background);
color: var(--body-text);
min-height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
padding: 0.25rem 0;
overflow-x: hidden;
transition: background-color 0.3s, color 0.3s;
font-size: 1rem;
line-height: 1.6;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<rect width="24" height="24" fill="#1a1a1a"/>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

View File

@ -24,3 +24,6 @@ uvicorn
pillow pillow
pymupdf pymupdf
markdown markdown
types-markdown
watchdog
websockets

View File

@ -5,7 +5,7 @@ cd $BASE_DIR
source myenv.sh source myenv.sh
cd poc cd poc_threefold
set +ex set +ex
open http://127.0.0.1:8001 open http://127.0.0.1:8001
python server.py python server.py