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

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>Project Mycelium</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 Project Mycelium. 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/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/ourworld.css">
<link rel="stylesheet" href="/static/menu.css">
<link rel="stylesheet" href="/static/login.css">
<link rel="stylesheet" href="/static/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 Project Mycelium 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>Project Mycelium</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 Project Mycelium</h2>
<p>Create your Project Mycelium 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">Mycelium</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,92 @@
<details>
<summary>What is web4?</summary>
<p>
No, Project Mycelium is a complementary Internet and lives with and on top of
the current Internet. From out of Project Mycelium 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 Project Mycelium 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
Project Mycelium 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 Project Mycelium 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 Project Mycelium 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 Project Mycelium Grid</a></li>
<li><a href="/user">Use capacity off the Project Mycelium 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, Project Mycelium is the next step to Web4.
Project Mycelium 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>
Project Mycelium 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 Project Mycelium 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 Project Mycelium 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 Project Mycelium.
</p>
</details>

View File

@@ -0,0 +1,94 @@
<details>
<summary>Is this a separate new Internet?</summary>
<p>
No, Project Mycelium is a complementary Internet and lives with and on top of
the current Internet. From out of Project Mycelium, 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 faking it?</summary>
<p>
We have been working on this for over 30 years and Project Mycelium is the
result of that work. We have a working product and a growing community
of farmers, users, and partners. We're for 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 CPUs and +17,000 GB
of storage available on the network.
<a
href="https://dashboard.grid.tf/#/tf-grid/node-statistics/"
target="_blank"
>Check out our stats on the ThreeFold Dashboard.</a
>
Our token TFT was used to build generation 1, 2 and 3 of the
ThreeFold Grid. 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, which is called the Mycelium Network, and we need a new token for this. There will never be more than 3 billion
INCA. Our partners will start selling new Project Mycelium Nodes by the end of November 2024
with a new reward scheme, so we're 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 Mycelium Network</a></li>
<li><a href="/user">Use capacity off the Mycelium Network</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 100% in control of their data and where there is 0 centralization.
Blockchain was the first step to Web3, Project Mycelium is the next step to Web4.
Project Mycelium is of course fully compatible with Web2 and Web3.
In Web4, each user has a personal Virtual Digital Assistant 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">Click here for more info about our Hero</a>.
</p>
</details>
<details>
<summary>How secure and private is my data?</summary>
<p>
Project Mycelium 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>Do you offer custom plans for businesses?</summary>
<p>
Yes, we have a network of integrators who can help you design a custom
plan.
</p>
</details>
<details>
<summary>What payment methods do you accept?</summary>
<p>We accept credit cards (v4) and our own tokens TFT and INCA.</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, Project Mycelium is the next step to Web4.
Project Mycelium 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>
Project Mycelium 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>
Project Mycelium 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>

View File

@@ -0,0 +1,23 @@
## About Our Platform
### What We Offer
Project Mycelium 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 Project Mycelium?
- **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,23 @@
{% include 'components/header.html' %}
<body>
{% include 'components/nav.html' %}
<main>
{% include 'components/globe.html' %}
{% set config = {
"title": "PROJECT MYCELIUM AUTONOMOUS INTERNET.",
"subtitle": "Building a decentralized internet, for a better world...",
"image": "static/cloud_dancing.png" }
%}
{% include 'components/hero_image.html' %}
{% set config = { "name": "threefold/faq_tf", "section_name": "threefold/faq_tf_section" } %}
{% 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 - Project Mycelium</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 Project Mycelium 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 Project Mycelium.</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>

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>

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": "PROJECT MYCELIUM INTERNET ROADMAP",
"subtitle": "Building a decentralized internet, for a better world",
"image": "static/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>

View File

@@ -0,0 +1,102 @@
from fastapi import FastAPI, Request, HTTPException
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
# 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):
raise RuntimeError(f"The source directory '{sources_dir}' does not exist.")
static_dir = f"{sources_dir}/static"
content_dir = f"{sources_dir}/content"
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
return re.sub(r'\[\[(.*?)\]\]', replace_content, content)
app = FastAPI()
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.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,162 @@
/* Import Google Fonts - if not already imported in ourworld.css */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Roboto:wght@300;400;500;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: 'Poppins', sans-serif;
font-weight: 200;
}
/* Heading styles */
.markdown-content h2,
.markdown-content h3 {
font-family: 'Poppins', sans-serif;
}
/* 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.5rem;
font-weight: 200;
line-height: 1.2;
margin-bottom: 1rem;
}
.markdown-content h3 {
font-size: 1.2rem;
font-weight: 200;
line-height: 1.3;
margin-bottom: 0.8rem;
}
/* Regular text styles */
.markdown-content p,
.markdown-content ul,
.markdown-content li {
font-family: 'Roboto', sans-serif;
font-weight: 200;
}
/* Make list items and paragraphs consistent size */
.markdown-content p,
.markdown-content li {
font-size: 1rem;
line-height: 1.3;
margin-bottom: 0.25rem;
}
/* List specific styling */
.markdown-content ul {
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-content li {
margin-bottom: 0.25rem;
}
/* FAQ section styling */
.faq-section {
flex: 1;
}
details {
border-bottom: 1px solid #ddd;
padding: 1em 0;
margin: 0;
}
summary {
font-family: 'Roboto', sans-serif;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
list-style: none;
margin: 0;
padding: 0;
outline: none;
}
.faq-section details summary {
color: #96989ead; /* Project Mycelium 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: 'Roboto', sans-serif;
margin: 0.5em 0 0 0;
color: #666;
font-size: 1rem;
line-height: 1.6;
font-weight: 200;
}
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=Poppins:wght@400;500;600;700&family=Roboto:wght@300;400;500;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 Poppins */
h1 {
font-family: 'Poppins', 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: 'Poppins', sans-serif;
font-size: 1.1rem;
margin-bottom: 1rem;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.2;
}
h3 {
font-family: 'Poppins', sans-serif;
font-size: 1rem;
margin-bottom: 0.8rem;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.3;
}
h4 {
font-family: 'Poppins', 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 Roboto */
p {
font-family: 'Roboto', sans-serif;
font-size: 1rem;
line-height: 1.6;
margin-bottom: 1.5rem;
max-width: 720px;
font-weight: 400;
}
/* Optional: styling for small or additional text */
small {
font-family: 'Roboto', sans-serif;
font-size: 0.9rem;
font-weight: 400;
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 Poppins */
nav {
font-family: 'Poppins', sans-serif;
}
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: 'Roboto', sans-serif;
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

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