Update.
This commit is contained in:
parent
925f91a17c
commit
d8a419f528
@ -1,4 +1,6 @@
|
||||
import aiosmtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.message import EmailMessage
|
||||
from aiohttp import web
|
||||
import logging
|
||||
@ -24,11 +26,12 @@ async def send_email(app: web.Application, recipient_email: str, subject: str, b
|
||||
logger.error("SMTP host or sender email not configured. Cannot send email.")
|
||||
return
|
||||
|
||||
msg = EmailMessage()
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg["From"] = smtp_sender_email
|
||||
msg["To"] = recipient_email
|
||||
msg["Subject"] = subject
|
||||
msg.set_content(body)
|
||||
html_part = MIMEText(body, 'html')
|
||||
msg.attach(html_part)
|
||||
|
||||
try:
|
||||
await aiosmtplib.send(
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
|
||||
import argparse
|
||||
from aiohttp import web
|
||||
import aiohttp_jinja2
|
||||
import jinja2
|
||||
@ -20,7 +22,7 @@ async def setup_services(app: web.Application):
|
||||
data_path = base_path.parent / "data"
|
||||
app["user_service"] = UserService(use_isolated_storage=True)
|
||||
app["config_service"] = ConfigService(data_path / "config.json")
|
||||
app["file_service"] = FileService(data_path / "user_files", data_path / "users.json")
|
||||
app["file_service"] = FileService(data_path / "user_files", app["user_service"])
|
||||
|
||||
# Setup aiojobs scheduler
|
||||
app["scheduler"] = aiojobs.Scheduler()
|
||||
@ -67,9 +69,15 @@ def create_app():
|
||||
return app
|
||||
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--port', type=int, default=8080)
|
||||
parser.add_argument('--hostname', default='0.0.0.0')
|
||||
args = parser.parse_args()
|
||||
app = create_app()
|
||||
web.run_app(app)
|
||||
web.run_app(app, host=args.hostname, port=args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -3,7 +3,8 @@ from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView
|
||||
from .views.upload import UploadView
|
||||
from .views.migrate import MigrateView
|
||||
from .views.admin import get_users, add_user, update_user_quota, delete_user, get_user_details, delete_team
|
||||
|
||||
from .views.editor import FileEditorView, FileContentView
|
||||
from .views.viewer import ViewerView
|
||||
|
||||
def setup_routes(app):
|
||||
app.router.add_view("/login", LoginView, name="login")
|
||||
@ -21,6 +22,12 @@ def setup_routes(app):
|
||||
app.router.add_view("/order", OrderView, name="order")
|
||||
app.router.add_view("/terms", SiteView, name="terms")
|
||||
app.router.add_view("/privacy", SiteView, name="privacy")
|
||||
app.router.add_view("/cookies", SiteView, name="cookies")
|
||||
app.router.add_view("/impressum", SiteView, name="impressum")
|
||||
app.router.add_view("/user_rights", SiteView, name="user_rights")
|
||||
app.router.add_view("/aup", SiteView, name="aup")
|
||||
app.router.add_view("/sla", SiteView, name="sla")
|
||||
app.router.add_view("/compliance", SiteView, name="compliance")
|
||||
app.router.add_view("/shared", SiteView, name="shared")
|
||||
app.router.add_view("/recent", SiteView, name="recent")
|
||||
app.router.add_view("/favorites", SiteView, name="favorites")
|
||||
@ -40,6 +47,10 @@ def setup_routes(app):
|
||||
app.router.add_post("/files/share_multiple", FileBrowserView, name="share_multiple_items")
|
||||
app.router.add_get("/shared_file/{share_id}", FileBrowserView.shared_file_handler, name="shared_file")
|
||||
app.router.add_get("/shared_file/{share_id}/download", FileBrowserView.download_shared_file_handler, name="download_shared_file")
|
||||
app.router.add_view("/editor", FileEditorView, name="file_editor")
|
||||
app.router.add_view("/viewer", ViewerView, name="file_viewer")
|
||||
app.router.add_get("/api/file/content", FileContentView, name="get_file_content")
|
||||
app.router.add_post("/api/file/save", FileEditorView, name="save_file_content")
|
||||
|
||||
# Admin API routes for user and team management
|
||||
app.router.add_get("/api/users", get_users, name="api_get_users")
|
||||
|
||||
@ -137,6 +137,63 @@ class FileService:
|
||||
logger.info(f"download_file: Successfully read file: {file_path}")
|
||||
return content, Path(file_path).name
|
||||
|
||||
async def read_file_content_binary(self, user_email: str, file_path: str) -> str | None:
|
||||
"""Reads file content as text for editing."""
|
||||
metadata = await self._load_metadata(user_email)
|
||||
if file_path not in metadata or metadata[file_path]["type"] != "file":
|
||||
logger.warning(f"read_file_content: File not found in metadata: {file_path}")
|
||||
return None
|
||||
item_meta = metadata[file_path]
|
||||
blob_loc = item_meta["blob_location"]
|
||||
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
|
||||
if not blob_path.exists():
|
||||
logger.warning(f"read_file_content: Blob not found: {blob_path}")
|
||||
return None
|
||||
try:
|
||||
async with aiofiles.open(blob_path, 'rb') as f:
|
||||
content = await f.read()
|
||||
logger.info(f"read_file_content: Successfully read file: {file_path}")
|
||||
return content
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(f"read_file_content: File is not a text file: {file_path}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
async def read_file_content(self, user_email: str, file_path: str) -> str | None:
|
||||
"""Reads file content as text for editing."""
|
||||
metadata = await self._load_metadata(user_email)
|
||||
if file_path not in metadata or metadata[file_path]["type"] != "file":
|
||||
logger.warning(f"read_file_content: File not found in metadata: {file_path}")
|
||||
return None
|
||||
item_meta = metadata[file_path]
|
||||
blob_loc = item_meta["blob_location"]
|
||||
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
|
||||
if not blob_path.exists():
|
||||
logger.warning(f"read_file_content: Blob not found: {blob_path}")
|
||||
return None
|
||||
try:
|
||||
async with aiofiles.open(blob_path, 'r', encoding='utf-8') as f:
|
||||
content = await f.read()
|
||||
logger.info(f"read_file_content: Successfully read file: {file_path}")
|
||||
return content
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(f"read_file_content: File is not a text file: {file_path}")
|
||||
return None
|
||||
|
||||
async def save_file_content(self, user_email: str, file_path: str, content: str) -> bool:
|
||||
"""Saves file content from editor."""
|
||||
try:
|
||||
content_bytes = content.encode('utf-8')
|
||||
success = await self.upload_file(user_email, file_path, content_bytes)
|
||||
if success:
|
||||
logger.info(f"save_file_content: Successfully saved file: {file_path}")
|
||||
return success
|
||||
except Exception as e:
|
||||
logger.error(f"save_file_content: Error saving file {file_path}: {e}")
|
||||
return False
|
||||
|
||||
async def delete_item(self, user_email: str, item_path: str) -> bool:
|
||||
"""Deletes a file or folder for the user."""
|
||||
metadata = await self._load_metadata(user_email)
|
||||
|
||||
@ -4,6 +4,7 @@ import hashlib
|
||||
import aiofiles
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from .lock_manager import get_lock_manager
|
||||
|
||||
|
||||
class StorageService:
|
||||
@ -11,6 +12,7 @@ class StorageService:
|
||||
def __init__(self, base_path: str = "data/user"):
|
||||
self.base_path = Path(base_path)
|
||||
self.base_path.mkdir(parents=True, exist_ok=True)
|
||||
self.lock_manager = get_lock_manager()
|
||||
|
||||
def _hash(self, value: str) -> str:
|
||||
return hashlib.sha256(value.encode()).hexdigest()
|
||||
@ -43,6 +45,8 @@ class StorageService:
|
||||
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
lock = await self.lock_manager.get_lock(str(file_path))
|
||||
async with lock:
|
||||
async with aiofiles.open(file_path, 'w') as f:
|
||||
await f.write(json.dumps(data, indent=2))
|
||||
|
||||
@ -58,6 +62,8 @@ class StorageService:
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
lock = await self.lock_manager.get_lock(str(file_path))
|
||||
async with lock:
|
||||
async with aiofiles.open(file_path, 'r') as f:
|
||||
content = await f.read()
|
||||
if not content:
|
||||
@ -74,6 +80,9 @@ class StorageService:
|
||||
if not self._validate_path(file_path, user_base):
|
||||
raise ValueError("Invalid path: directory traversal detected")
|
||||
|
||||
if file_path.exists():
|
||||
lock = await self.lock_manager.get_lock(str(file_path))
|
||||
async with lock:
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
return True
|
||||
@ -98,6 +107,8 @@ class StorageService:
|
||||
results = []
|
||||
for json_file in user_base.rglob("*.json"):
|
||||
if self._validate_path(json_file, user_base):
|
||||
lock = await self.lock_manager.get_lock(str(json_file))
|
||||
async with lock:
|
||||
async with aiofiles.open(json_file, 'r') as f:
|
||||
content = await f.read()
|
||||
results.append(json.loads(content))
|
||||
@ -122,6 +133,7 @@ class UserStorageManager:
|
||||
|
||||
def __init__(self):
|
||||
self.storage = StorageService()
|
||||
self.lock_manager = get_lock_manager()
|
||||
|
||||
async def save_user(self, user_email: str, user_data: Dict[str, Any]) -> bool:
|
||||
return await self.storage.save(user_email, user_email, user_data)
|
||||
@ -146,6 +158,8 @@ class UserStorageManager:
|
||||
if user_dir.is_dir():
|
||||
user_files = list(user_dir.rglob("*.json"))
|
||||
for user_file in user_files:
|
||||
lock = await self.lock_manager.get_lock(str(user_file))
|
||||
async with lock:
|
||||
async with aiofiles.open(user_file, 'r') as f:
|
||||
content = await f.read()
|
||||
user_data = json.loads(content)
|
||||
@ -165,6 +179,8 @@ class UserStorageManager:
|
||||
if user_dir.is_dir():
|
||||
user_files = list(user_dir.rglob("*.json"))
|
||||
for user_file in user_files:
|
||||
lock = await self.lock_manager.get_lock(str(user_file))
|
||||
async with lock:
|
||||
async with aiofiles.open(user_file, 'r') as f:
|
||||
content = await f.read()
|
||||
all_users.append(json.loads(content))
|
||||
|
||||
@ -5,11 +5,13 @@ import bcrypt
|
||||
import secrets
|
||||
import datetime
|
||||
from .storage_service import UserStorageManager
|
||||
from .lock_manager import get_lock_manager
|
||||
|
||||
|
||||
class UserService:
|
||||
def __init__(self, users_path: Path = None, use_isolated_storage: bool = True):
|
||||
self.use_isolated_storage = use_isolated_storage
|
||||
self.lock_manager = get_lock_manager()
|
||||
|
||||
if use_isolated_storage:
|
||||
self._storage_manager = UserStorageManager()
|
||||
@ -27,9 +29,11 @@ class UserService:
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
def _save_users(self):
|
||||
async def _save_users(self):
|
||||
if self.use_isolated_storage:
|
||||
return
|
||||
lock = await self.lock_manager.get_lock(str(self._users_path))
|
||||
async with lock:
|
||||
with open(self._users_path, "w") as f:
|
||||
json.dump(self._users, f, indent=4)
|
||||
|
||||
@ -80,7 +84,7 @@ class UserService:
|
||||
await self._storage_manager.save_user(email, user)
|
||||
else:
|
||||
self._users.append(user)
|
||||
self._save_users()
|
||||
await self._save_users()
|
||||
|
||||
return user
|
||||
|
||||
@ -98,7 +102,7 @@ class UserService:
|
||||
if self.use_isolated_storage:
|
||||
await self._storage_manager.save_user(email, user)
|
||||
else:
|
||||
self._save_users()
|
||||
await self._save_users()
|
||||
|
||||
return user
|
||||
|
||||
@ -109,7 +113,7 @@ class UserService:
|
||||
initial_len = len(self._users)
|
||||
self._users = [user for user in self._users if user["email"] != email]
|
||||
if len(self._users) < initial_len:
|
||||
self._save_users()
|
||||
await self._save_users()
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -126,7 +130,7 @@ class UserService:
|
||||
self._users = [user for user in self._users if user.get("parent_email") != parent_email]
|
||||
deleted_count = initial_len - len(self._users)
|
||||
if deleted_count > 0:
|
||||
self._save_users()
|
||||
await self._save_users()
|
||||
|
||||
return deleted_count
|
||||
|
||||
@ -160,7 +164,7 @@ class UserService:
|
||||
if self.use_isolated_storage:
|
||||
await self._storage_manager.save_user(email, user)
|
||||
else:
|
||||
self._save_users()
|
||||
await self._save_users()
|
||||
|
||||
return token
|
||||
|
||||
@ -192,7 +196,7 @@ class UserService:
|
||||
if self.use_isolated_storage:
|
||||
await self._storage_manager.save_user(email, user)
|
||||
else:
|
||||
self._save_users()
|
||||
await self._save_users()
|
||||
|
||||
return True
|
||||
|
||||
@ -206,4 +210,4 @@ class UserService:
|
||||
if self.use_isolated_storage:
|
||||
await self._storage_manager.save_user(email, user)
|
||||
else:
|
||||
self._save_users()
|
||||
await self._save_users()
|
||||
|
||||
@ -1,64 +1,72 @@
|
||||
:root {
|
||||
--primary-color: #4A90E2; /* Blue from image */
|
||||
--accent-color: #50E3C2; /* Greenish-blue from image */
|
||||
--secondary-color: #B8C2CC; /* Light grey-blue */
|
||||
--background-color: #F8F8F8; /* Very light grey background */
|
||||
--text-color: #333333; /* Darker text */
|
||||
--light-text-color: #666666; /* Lighter text for descriptions */
|
||||
--border-color: #E0E0E0; /* Light grey border */
|
||||
--primary-color: #0066FF; /* Vibrant blue */
|
||||
--accent-color: #00D4FF; /* Bright cyan accent */
|
||||
--secondary-color: #F7FAFC; /* Very light blue-grey */
|
||||
--background-color: #FFFFFF; /* Pure white background */
|
||||
--text-color: #1A202C; /* Dark blue-grey */
|
||||
--light-text-color: #718096; /* Medium grey for descriptions */
|
||||
--border-color: #E2E8F0; /* Light grey border */
|
||||
--card-background: #FFFFFF; /* White for cards */
|
||||
--shadow-color: rgba(0, 0, 0, 0.08); /* Subtle shadow */
|
||||
--shadow-color: rgba(0, 0, 0, 0.05); /* Very subtle shadow */
|
||||
|
||||
/* Button specific variables, using accent for primary CTAs */
|
||||
/* Button specific variables */
|
||||
--btn-primary-bg: var(--primary-color);
|
||||
--btn-primary-text: #FFFFFF;
|
||||
--btn-primary-hover-bg: #3A7BD5; /* Slightly darker blue */
|
||||
--btn-secondary-bg: #E0E0E0; /* Light grey for secondary buttons */
|
||||
--btn-primary-hover-bg: #0052CC; /* Darker blue */
|
||||
--btn-secondary-bg: #EDF2F7; /* Light grey */
|
||||
--btn-secondary-text: var(--text-color);
|
||||
--btn-secondary-hover-bg: #BDBDBD; /* Darker grey for secondary hover */
|
||||
--btn-secondary-hover-bg: #E2E8F0; /* Darker grey */
|
||||
--btn-outline-border: var(--primary-color);
|
||||
--btn-outline-text: var(--primary-color);
|
||||
--btn-outline-hover-bg: rgba(74, 144, 226, 0.1); /* Light blue hover */
|
||||
--btn-outline-hover-bg: rgba(0, 102, 255, 0.05); /* Very light blue hover */
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: 'Roboto', sans-serif; /* Using a more modern font */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.6; /* Improve readability */
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* General typography */
|
||||
h1 {
|
||||
font-size: 2.2rem; /* Slightly larger heading */
|
||||
font-size: 3.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.2rem;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.6rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--light-text-color);
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Card-like styling for sections */
|
||||
@ -147,96 +155,100 @@ p {
|
||||
}
|
||||
|
||||
/* Header and Navigation */
|
||||
header {
|
||||
background-color: var(--card-background); /* White background for header */
|
||||
box-shadow: 0 2px 4px var(--shadow-color);
|
||||
padding: 15px 20px;
|
||||
.site-header {
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
nav {
|
||||
.site-nav {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
min-height: 70px;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
nav .logo {
|
||||
.brand {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
nav .logo img {
|
||||
height: 30px; /* Adjust logo size */
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
nav .nav-links {
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 30px;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav .nav-links a {
|
||||
.nav-menu li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-menu a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
font-size: 1rem;
|
||||
transition: color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
nav .nav-links a:hover {
|
||||
.nav-menu a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary-nav {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s ease;
|
||||
font-size: 1rem;
|
||||
transition: color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-primary-nav:hover {
|
||||
background-color: var(--btn-primary-hover-bg);
|
||||
}
|
||||
|
||||
.btn-outline-nav {
|
||||
background-color: transparent;
|
||||
.nav-link:hover {
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-outline-nav:hover {
|
||||
background-color: rgba(74, 144, 226, 0.1);
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Main content area */
|
||||
main {
|
||||
flex-grow: 1; /* Allow main content to take available space */
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
max-width: 1200px; /* Match header max-width */
|
||||
margin: 0 auto; /* Center the main content */
|
||||
padding: 40px 20px; /* Add padding */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--background-color); /* Light background for main content */
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
/* Form specific styles */
|
||||
@ -244,17 +256,17 @@ main {
|
||||
max-width: 450px;
|
||||
margin: 50px auto;
|
||||
background-color: var(--card-background);
|
||||
border-radius: 12px; /* Slightly more rounded corners */
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); /* Softer, more prominent shadow */
|
||||
padding: 3rem; /* Increased padding */
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
padding: 2.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-container h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 35px; /* Increased margin */
|
||||
margin-bottom: 2rem;
|
||||
color: var(--text-color);
|
||||
font-size: 2.2rem; /* Slightly larger heading */
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@ -309,16 +321,17 @@ main {
|
||||
.btn-small,
|
||||
.btn-danger {
|
||||
display: inline-block;
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
padding: 0.65rem 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@ -330,6 +343,8 @@ main {
|
||||
.btn-primary:hover {
|
||||
background-color: var(--btn-primary-hover-bg);
|
||||
border-color: var(--btn-primary-hover-bg);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@ -342,6 +357,7 @@ main {
|
||||
background-color: var(--btn-outline-hover-bg);
|
||||
color: var(--btn-outline-text);
|
||||
border-color: var(--btn-outline-border);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
@ -372,28 +388,81 @@ main {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for base.css */
|
||||
@media (max-width: 768px) {
|
||||
nav {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 992px) {
|
||||
.nav-menu {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
nav .logo {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
nav .nav-links {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.btn-primary-nav {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
main {
|
||||
padding: 20px 15px;
|
||||
|
||||
.nav-container {
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.site-nav {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
flex-wrap: wrap;
|
||||
min-height: auto;
|
||||
padding: 10px 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.brand {
|
||||
flex: 1 1 100%;
|
||||
justify-content: center;
|
||||
order: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex: 1 1 100%;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
order: 2;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-menu a {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
flex: 1 1 100%;
|
||||
justify-content: center;
|
||||
order: 3;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.brand-text {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.nav-menu a {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-actions .btn-primary,
|
||||
.nav-actions .btn-outline {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,72 @@
|
||||
/* Content pages main wrapper */
|
||||
main.content-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
/* Content sections for legal and information pages */
|
||||
.content-section {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.content-section h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.75rem;
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.content-section p {
|
||||
margin-bottom: 1.25rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.content-section ul {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.content-section li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content-section table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 2rem 0;
|
||||
background-color: var(--card-background);
|
||||
}
|
||||
|
||||
.content-section table th,
|
||||
.content-section table td {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.content-section table th {
|
||||
background-color: var(--secondary-color);
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* General styles for hero sections on content pages */
|
||||
.hero-intro {
|
||||
padding: 20px;
|
||||
|
||||
@ -10,15 +10,25 @@
|
||||
}
|
||||
|
||||
.dashboard-sidebar {
|
||||
flex: 0 0 250px; /* Fixed width sidebar */
|
||||
flex: 0 0 250px;
|
||||
background-color: var(--card-background);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px var(--shadow-color);
|
||||
padding: 20px;
|
||||
position: sticky; /* Make sidebar sticky */
|
||||
top: 100px; /* Adjust based on header height */
|
||||
max-height: calc(100vh - 120px); /* Adjust based on header/footer */
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.dashboard-sidebar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dashboard-sidebar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.sidebar-menu ul {
|
||||
@ -162,6 +172,7 @@
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.file-list-table th,
|
||||
@ -181,6 +192,9 @@
|
||||
|
||||
.file-list-table td {
|
||||
color: var(--light-text-color);
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.file-list-table tr:last-child td {
|
||||
@ -200,6 +214,43 @@
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
|
||||
.file-list-table a {
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.file-list-table th:first-child,
|
||||
.file-list-table td:first-child {
|
||||
width: 40px;
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
.file-list-table th:nth-child(2),
|
||||
.file-list-table td:nth-child(2) {
|
||||
width: 40%;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.file-list-table th:nth-child(3),
|
||||
.file-list-table td:nth-child(3) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.file-list-table th:nth-child(4),
|
||||
.file-list-table td:nth-child(4) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.file-list-table th:nth-child(5),
|
||||
.file-list-table td:nth-child(5) {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.file-list-table th:last-child,
|
||||
.file-list-table td:last-child {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 992px) {
|
||||
.dashboard-layout {
|
||||
@ -249,4 +300,8 @@
|
||||
padding: 10px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.file-list-table td {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,20 @@
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
display: none !important;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
@ -10,6 +25,10 @@
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--card-background);
|
||||
margin: 10% auto;
|
||||
@ -161,6 +180,7 @@
|
||||
.file-list-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.file-list-table th {
|
||||
@ -177,6 +197,9 @@
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.file-list-table tr:hover {
|
||||
@ -197,12 +220,46 @@
|
||||
.file-list-table a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.file-list-table a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.file-list-table th:first-child,
|
||||
.file-list-table td:first-child {
|
||||
width: 40px;
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
.file-list-table th:nth-child(2),
|
||||
.file-list-table td:nth-child(2) {
|
||||
width: 40%;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.file-list-table th:nth-child(3),
|
||||
.file-list-table td:nth-child(3) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.file-list-table th:nth-child(4),
|
||||
.file-list-table td:nth-child(4) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.file-list-table th:nth-child(5),
|
||||
.file-list-table td:nth-child(5) {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.file-list-table th:last-child,
|
||||
.file-list-table td:last-child {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
@ -227,6 +284,10 @@
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.file-list-table td {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@ -1,14 +1,70 @@
|
||||
footer {
|
||||
background-color: var(--card-background); /* Consistent with header */
|
||||
color: var(--light-text-color); /* Subtle text color */
|
||||
padding: 1.5rem 2rem;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--border-color); /* Separator from content */
|
||||
margin-top: auto; /* Push footer to the bottom */
|
||||
box-shadow: 0 -2px 4px var(--shadow-color); /* Subtle shadow upwards */
|
||||
background-color: var(--card-background);
|
||||
color: var(--light-text-color);
|
||||
padding: 2rem 2rem 1rem 2rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: auto;
|
||||
box-shadow: 0 -2px 4px var(--shadow-color);
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0; /* Remove default paragraph margin */
|
||||
font-size: 0.9rem;
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 2rem auto;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.footer-section h4 {
|
||||
color: var(--text-color);
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.footer-section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer-section ul li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-section ul li a {
|
||||
color: var(--light-text-color);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.footer-section ul li a:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.footer-bottom p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,135 +1,260 @@
|
||||
/* Styles for the Homepage (index.html) */
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
/* Removed background-color and box-shadow to match image */
|
||||
margin-bottom: 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 100px 20px 80px;
|
||||
background: linear-gradient(135deg, #F7FAFC 0%, #FFFFFF 100%);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 3.5rem;
|
||||
font-size: 3.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 0.5rem; /* Adjusted margin to match image */
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.hero-section p {
|
||||
font-size: 1.3rem;
|
||||
.hero-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: var(--light-text-color);
|
||||
max-width: 800px;
|
||||
margin: 0 auto 2.5rem auto;
|
||||
line-height: 1.5;
|
||||
max-width: 700px;
|
||||
margin: 0 auto 2.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
max-width: 1200px;
|
||||
margin: 40px auto; /* Adjusted margin to separate from text */
|
||||
}
|
||||
|
||||
.benefit-card {
|
||||
background-color: var(--card-background);
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px var(--shadow-color);
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
||||
.hero-ctas {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
min-height: 250px; /* Ensure cards have a consistent height */
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.benefit-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
|
||||
.hero-btn {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.benefit-card img.icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-bottom: 20px;
|
||||
/* Icons in the image are colored, not inheriting text color */
|
||||
.hero-subtext {
|
||||
font-size: 0.875rem;
|
||||
color: var(--light-text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.benefit-card h3 {
|
||||
font-size: 1.5rem;
|
||||
/* Features Section */
|
||||
.features-section {
|
||||
padding: 80px 20px;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.features-header {
|
||||
text-align: center;
|
||||
max-width: 700px;
|
||||
margin: 0 auto 60px;
|
||||
}
|
||||
|
||||
.features-header h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.features-header p {
|
||||
font-size: 1.125rem;
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 2.5rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 1.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.benefit-card p {
|
||||
.feature-card p {
|
||||
font-size: 1rem;
|
||||
color: var(--light-text-color);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Specific card colors from the image */
|
||||
.family-card {
|
||||
background-color: #D0E6F0; /* Light blue */
|
||||
/* Use Cases Section */
|
||||
.use-cases-section {
|
||||
padding: 80px 20px;
|
||||
background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.professional-card {
|
||||
background-color: #F0E0D0; /* Light orange */
|
||||
.use-cases-section h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 3rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.student-card {
|
||||
background-color: #D0F0D0; /* Light green */
|
||||
.use-cases-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.find-plan-btn {
|
||||
margin-top: 40px;
|
||||
padding: 15px 30px;
|
||||
font-size: 1.1rem;
|
||||
.use-case-card {
|
||||
background-color: #FFFFFF;
|
||||
padding: 2.5rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.use-case-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.use-case-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.use-case-card h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
border-radius: 5px;
|
||||
background-color: #4A90E2; /* Blue from the image */
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.use-case-card p {
|
||||
font-size: 1rem;
|
||||
color: var(--light-text-color);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.cta-section {
|
||||
padding: 100px 20px;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0052CC 100%);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cta-content {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cta-section h2 {
|
||||
font-size: 2.75rem;
|
||||
color: white;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cta-section p {
|
||||
font-size: 1.25rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.cta-btn {
|
||||
padding: 1rem 2.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
background-color: white;
|
||||
color: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: background-color 0.3s ease;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.find-plan-btn:hover {
|
||||
background-color: #3A7BD5; /* Darker blue on hover */
|
||||
.cta-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Responsive adjustments for index.css */
|
||||
/* Responsive Design */
|
||||
@media (max-width: 992px) {
|
||||
.hero-section h1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
.hero-section p {
|
||||
font-size: 1.2rem;
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.features-header h2,
|
||||
.use-cases-section h2,
|
||||
.cta-section h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 40px 15px;
|
||||
padding: 60px 20px 50px;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.hero-section p {
|
||||
font-size: 1rem;
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
.benefits-grid {
|
||||
grid-template-columns: 1fr; /* Stack cards on small screens */
|
||||
|
||||
.hero-ctas {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.benefit-card {
|
||||
padding: 25px;
|
||||
}
|
||||
.find-plan-btn {
|
||||
|
||||
.hero-btn {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.use-cases-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-section,
|
||||
.use-cases-section {
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
padding: 60px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,11 +262,14 @@
|
||||
.hero-section h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.hero-section p {
|
||||
font-size: 0.9rem;
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.hero-ctas .btn-primary, .hero-ctas .btn-outline {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.features-header h2,
|
||||
.use-cases-section h2,
|
||||
.cta-section h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
export function showUploadModal() {
|
||||
document.getElementById('upload-modal').style.display = 'block';
|
||||
// Clear previous selections and progress
|
||||
const modal = document.getElementById('upload-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('show');
|
||||
}
|
||||
document.getElementById('selected-files-preview').innerHTML = '';
|
||||
document.getElementById('upload-progress-container').innerHTML = '';
|
||||
document.getElementById('file-input-multiple').value = ''; // Clear selected files
|
||||
document.getElementById('file-input-multiple').value = '';
|
||||
document.getElementById('start-upload-btn').disabled = true;
|
||||
}
|
||||
|
||||
@ -13,6 +15,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const startUploadBtn = document.getElementById('start-upload-btn');
|
||||
const uploadProgressContainer = document.getElementById('upload-progress-container');
|
||||
|
||||
if (!fileInput || !selectedFilesPreview || !startUploadBtn || !uploadProgressContainer) {
|
||||
console.error('Upload elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
let filesToUpload = [];
|
||||
|
||||
fileInput.addEventListener('change', (event) => {
|
||||
@ -55,13 +62,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
async function uploadFiles(files) {
|
||||
startUploadBtn.disabled = true; // Disable button during upload
|
||||
uploadProgressContainer.innerHTML = ''; // Clear previous progress
|
||||
startUploadBtn.disabled = true;
|
||||
uploadProgressContainer.innerHTML = '';
|
||||
|
||||
const currentPath = new URLSearchParams(window.location.search).get('path') || '';
|
||||
console.log('Uploading to directory:', currentPath || '(root)');
|
||||
|
||||
for (const file of files) {
|
||||
let completedUploads = 0;
|
||||
let totalFiles = files.length;
|
||||
let hasErrors = false;
|
||||
|
||||
const uploadPromises = files.map(file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.append('current_path', currentPath);
|
||||
formData.append('file', file);
|
||||
|
||||
const progressBarContainer = document.createElement('div');
|
||||
@ -71,46 +85,71 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<div class="progress-bar-wrapper">
|
||||
<div class="progress-bar" id="progress-${file.name.replace(/\./g, '-')}" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="progress-text-${file.name.replace(/\./g, '-')}" >0%</div>
|
||||
<div class="progress-text" id="progress-text-${file.name.replace(/\./g, '-')}">0%</div>
|
||||
`;
|
||||
uploadProgressContainer.appendChild(progressBarContainer);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `/files/upload?current_path=${encodeURIComponent(currentPath)}`, true);
|
||||
xhr.open('POST', `/files/upload`, true);
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const percent = (event.loaded / event.total) * 100;
|
||||
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `${percent}%`;
|
||||
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `${Math.round(percent)}%`;
|
||||
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
|
||||
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
|
||||
if (progressBar) progressBar.style.width = `${percent}%`;
|
||||
if (progressText) progressText.textContent = `${Math.round(percent)}%`;
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status === 200) {
|
||||
console.log(`File ${file.name} uploaded successfully.`);
|
||||
// Update progress to 100% on completion
|
||||
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `100%`;
|
||||
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `100% (Done)`;
|
||||
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
|
||||
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
|
||||
if (progressBar) progressBar.style.width = '100%';
|
||||
if (progressText) progressText.textContent = '100% (Done)';
|
||||
resolve();
|
||||
} else {
|
||||
console.error(`Error uploading ${file.name}: ${xhr.statusText}`);
|
||||
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Failed (${xhr.status})`;
|
||||
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
|
||||
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
|
||||
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
|
||||
if (progressText) progressText.textContent = `Failed (${xhr.status})`;
|
||||
if (progressBar) progressBar.style.backgroundColor = 'red';
|
||||
hasErrors = true;
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
console.error(`Network error uploading ${file.name}.`);
|
||||
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Network Error`;
|
||||
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
|
||||
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
|
||||
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
|
||||
if (progressText) progressText.textContent = 'Network Error';
|
||||
if (progressBar) progressBar.style.backgroundColor = 'red';
|
||||
hasErrors = true;
|
||||
reject(new Error('Network error'));
|
||||
});
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
// After all files are sent, refresh the page to show new files
|
||||
// A small delay to allow server to process and update file list
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.allSettled(uploadPromises);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const pathParam = currentUrl.searchParams.get('path');
|
||||
if (pathParam) {
|
||||
window.location.href = `/files?path=${encodeURIComponent(pathParam)}`;
|
||||
} else {
|
||||
window.location.href = '/files';
|
||||
}
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error during upload:', error);
|
||||
startUploadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -88,17 +88,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Helper functions for modals
|
||||
function showNewFolderModal() {
|
||||
document.getElementById('new-folder-modal').style.display = 'block';
|
||||
const modal = document.getElementById('new-folder-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).style.display = 'none';
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('show');
|
||||
}
|
||||
window.closeModal = closeModal; // Make it globally accessible
|
||||
}
|
||||
window.closeModal = closeModal;
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.style.display = 'none';
|
||||
event.target.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,14 +119,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const loading = document.getElementById('share-loading');
|
||||
const shareLinkInput = document.getElementById('share-link-input');
|
||||
const shareFileName = document.getElementById('share-file-name');
|
||||
const shareLinksList = document.getElementById('share-links-list'); // New element for multiple links
|
||||
const shareLinksList = document.getElementById('share-links-list');
|
||||
|
||||
// Clear previous content
|
||||
shareLinkInput.value = '';
|
||||
if (shareLinksList) shareLinksList.innerHTML = '';
|
||||
linkContainer.style.display = 'none';
|
||||
loading.style.display = 'block';
|
||||
modal.style.display = 'block';
|
||||
modal.classList.add('show');
|
||||
|
||||
if (paths.length === 1) {
|
||||
shareFileName.textContent = `Sharing: ${names[0]}`;
|
||||
@ -180,7 +185,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const deleteMessage = document.getElementById('delete-message');
|
||||
const deleteModal = document.getElementById('delete-modal');
|
||||
|
||||
// Clear previous hidden inputs
|
||||
deleteForm.querySelectorAll('input[name="paths[]"]').forEach(input => input.remove());
|
||||
|
||||
if (Array.isArray(paths) && paths.length > 1) {
|
||||
@ -199,7 +203,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
deleteMessage.textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
|
||||
deleteForm.action = `/files/delete/${path}`;
|
||||
}
|
||||
deleteModal.style.display = 'block';
|
||||
deleteModal.classList.add('show');
|
||||
}
|
||||
|
||||
// Selection and action buttons
|
||||
|
||||
@ -1,3 +1,31 @@
|
||||
<footer aria-label="Site information and copyright">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>Legal</h4>
|
||||
<ul>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/cookies">Cookie Policy</a></li>
|
||||
<li><a href="/terms">Terms of Service</a></li>
|
||||
<li><a href="/impressum">Impressum</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Policies</h4>
|
||||
<ul>
|
||||
<li><a href="/aup">Acceptable Use Policy</a></li>
|
||||
<li><a href="/sla">Service Level Agreement</a></li>
|
||||
<li><a href="/compliance">Security & Compliance</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>User Rights</h4>
|
||||
<ul>
|
||||
<li><a href="/user_rights">Data Access & Deletion</a></li>
|
||||
<li><a href="/support">Contact Support</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2025 Retoors. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -1,22 +1,26 @@
|
||||
<header>
|
||||
<nav aria-label="Main navigation">
|
||||
<a href="/" class="logo" aria-label="HomeBase Storage home">
|
||||
<img src="/static/images/retoors-logo.svg" alt="HomeBase Storage" />
|
||||
<span>HomeBase Storage</span>
|
||||
<header class="site-header">
|
||||
<nav class="site-nav" aria-label="Main navigation">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="brand" aria-label="Retoor's Cloud Solutions home">
|
||||
<span class="brand-text">Retoor's</span>
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/solutions" aria-label="Our Solutions">Solutions</a></li>
|
||||
<li><a href="/pricing" aria-label="Pricing Plans">Pricing</a></li>
|
||||
<li><a href="/security" aria-label="Security Information">Security</a></li>
|
||||
<li><a href="/support" aria-label="Support Page">Support</a></li>
|
||||
<ul class="nav-menu">
|
||||
<li><a href="/solutions">Solutions</a></li>
|
||||
<li><a href="/pricing">Pricing</a></li>
|
||||
<li><a href="/security">Security</a></li>
|
||||
{% if request['user'] %}
|
||||
<li><a href="/dashboard" aria-label="User Dashboard">Dashboard</a></li>
|
||||
<li><a href="/files" aria-label="File Browser">File Browser</a></li>
|
||||
<li><a href="/logout" class="btn-primary-nav" aria-label="Logout">Logout</a></li>
|
||||
{% else %}
|
||||
<li><a href="/login" class="btn-outline-nav" aria-label="Sign In to your account">Sign In</a></li>
|
||||
<li><a href="/register" class="btn-primary-nav" aria-label="Start your free trial">Start Your Free Trial</a></li>
|
||||
<li><a href="/files">My Files</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
{% if request['user'] %}
|
||||
<a href="/files" class="nav-link">Dashboard</a>
|
||||
<a href="/logout" class="btn-outline">Logout</a>
|
||||
{% else %}
|
||||
<a href="/login" class="nav-link">Sign In</a>
|
||||
<a href="/register" class="btn-primary">Get Started Free</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@ -3,18 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Retoors Storage{% endblock %}</title>
|
||||
<title>{% block title %}Retoors Cloud Solutions{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/base.css">
|
||||
<link rel="stylesheet" href="/static/css/components/footer.css">
|
||||
<link rel="stylesheet" href="/static/css/components/content_pages.css"> {# Added for content page styling #}
|
||||
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
{% include 'components/navigation.html' %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% include 'components/footer.html' %}
|
||||
{% include 'components/cookie_banner.html' %}
|
||||
<script src="/static/js/main.js" type="module"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
{% extends "layouts/dashboard.html" %}
|
||||
|
||||
{% block title %}My Files - Retoor's Cloud Solutions{% endblock %}
|
||||
{% block title %}{% if current_path %}{{ current_path.split('/')[-1] }} - Retoor's Cloud Solutions{% else %}My Files - Retoor's Cloud Solutions{% endif %}{% endblock %}
|
||||
|
||||
{% block dashboard_head %}
|
||||
<link rel="stylesheet" href="/static/css/components/file_browser.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block page_title %}My Files{% endblock %}
|
||||
{% block page_title %}{% if current_path %}{{ current_path.split('/')[-1] }}{% else %}My Files{% endif %}{% endblock %}
|
||||
|
||||
{% block dashboard_actions %}
|
||||
<button class="btn-primary" id="new-folder-btn">+ New</button>
|
||||
@ -55,8 +55,14 @@
|
||||
<a href="/files?path={{ item.path }}">{{ item.name }}</a>
|
||||
{% else %}
|
||||
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
|
||||
{% if item.is_editable %}
|
||||
<a href="/editor?path={{ item.path }}">{{ item.name }}</a>
|
||||
{% elif item.is_viewable %}
|
||||
<a href="/viewer?path={{item.path}}">{{ item.name }}</a>
|
||||
{% else %}
|
||||
{{ item.name }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ item.last_modified[:10] }}</td>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}Solutions for Everyone - Retoor's Cloud Solutions{% endblock %}
|
||||
{% block title %}Secure Cloud Storage for Everyone - Retoor's Cloud Solutions{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/components/index.css">
|
||||
@ -9,26 +9,114 @@
|
||||
{% block content %}
|
||||
<main>
|
||||
<section class="hero-section">
|
||||
<h1>Solutions for Everyone</h1>
|
||||
<p>Solutions for Everyone</p>
|
||||
<div class="benefits-grid">
|
||||
<div class="benefit-card family-card">
|
||||
<img src="/static/images/icon-families.svg" alt="Families Icon" class="icon">
|
||||
<div class="hero-content">
|
||||
<h1>Your files, safe and accessible everywhere</h1>
|
||||
<p class="hero-subtitle">Store, sync, and share your files with enterprise-grade security. Access from any device, anytime, anywhere.</p>
|
||||
<div class="hero-ctas">
|
||||
<a href="/register" class="btn-primary hero-btn">Get Started Free</a>
|
||||
<a href="/pricing" class="btn-outline hero-btn">View Pricing</a>
|
||||
</div>
|
||||
<p class="hero-subtext">No credit card required • 10 GB free storage</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-header">
|
||||
<h2>Everything you need to work smarter</h2>
|
||||
<p>Powerful features designed to keep your data secure and accessible</p>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Bank-level Security</h3>
|
||||
<p>256-bit AES encryption and TLS 1.3 protocol ensure your files stay private and protected</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
||||
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Access Anywhere</h3>
|
||||
<p>Seamlessly sync across all your devices. Desktop, mobile, or web - your files are always there</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Easy Sharing</h3>
|
||||
<p>Share files and folders with anyone using secure links. Control access with permissions</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>High Performance</h3>
|
||||
<p>Lightning-fast upload and download speeds powered by enterprise infrastructure</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Auto Backup</h3>
|
||||
<p>Never lose important files. Automatic backups keep multiple versions of your documents</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>24/7 Support</h3>
|
||||
<p>Expert support team ready to help whenever you need. Email, chat, and phone support available</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="use-cases-section">
|
||||
<h2>Perfect for every need</h2>
|
||||
<div class="use-cases-grid">
|
||||
<div class="use-case-card">
|
||||
<img src="/static/images/icon-families.svg" alt="Families Icon" class="use-case-icon">
|
||||
<h3>For Families</h3>
|
||||
<p>Securely backup and share precious photos and videos. Keep fond memories safe for generations.</p>
|
||||
<p>Keep precious memories safe. Share photos and videos with family members securely.</p>
|
||||
</div>
|
||||
<div class="benefit-card professional-card">
|
||||
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="icon">
|
||||
<div class="use-case-card">
|
||||
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="use-case-icon">
|
||||
<h3>For Professionals</h3>
|
||||
<p>Organize important work documents, collaborate in teams, and access files from anywhere.</p>
|
||||
<p>Collaborate on projects, share documents, and work from anywhere with confidence.</p>
|
||||
</div>
|
||||
<div class="benefit-card student-card">
|
||||
<img src="/static/images/icon-students.svg" alt="Students Icon" class="icon">
|
||||
<div class="use-case-card">
|
||||
<img src="/static/images/icon-students.svg" alt="Students Icon" class="use-case-icon">
|
||||
<h3>For Students</h3>
|
||||
<p>Store projects, notes, research papers. Access study materials across your devices.</p>
|
||||
<p>Store all your coursework, projects, and study materials in one secure place.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/solutions" class="btn-primary find-plan-btn">Find Your Perfect Plan</a>
|
||||
</section>
|
||||
|
||||
<section class="cta-section">
|
||||
<div class="cta-content">
|
||||
<h2>Start storing your files securely today</h2>
|
||||
<p>Join thousands of users who trust Retoor's Cloud Solutions</p>
|
||||
<a href="/register" class="btn-primary cta-btn">Create Free Account</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -38,6 +38,18 @@ class SiteView(web.View):
|
||||
return await self.terms()
|
||||
elif self.request.path == "/privacy":
|
||||
return await self.privacy()
|
||||
elif self.request.path == "/cookies":
|
||||
return await self.cookies()
|
||||
elif self.request.path == "/impressum":
|
||||
return await self.impressum()
|
||||
elif self.request.path == "/user_rights":
|
||||
return await self.user_rights()
|
||||
elif self.request.path == "/aup":
|
||||
return await self.aup()
|
||||
elif self.request.path == "/sla":
|
||||
return await self.sla()
|
||||
elif self.request.path == "/compliance":
|
||||
return await self.compliance()
|
||||
elif self.request.path == "/shared":
|
||||
return await self.shared()
|
||||
elif self.request.path == "/recent":
|
||||
@ -92,6 +104,36 @@ class SiteView(web.View):
|
||||
"pages/privacy.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
async def cookies(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
"pages/cookies.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
async def impressum(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
"pages/impressum.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
async def user_rights(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
"pages/user_rights.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
async def aup(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
"pages/aup.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
async def sla(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
"pages/sla.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
async def compliance(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
"pages/compliance.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
@login_required
|
||||
async def shared(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
@ -141,6 +183,15 @@ class FileBrowserView(web.View):
|
||||
path = self.request.query.get("path", "")
|
||||
files = await file_service.list_files(user_email, path)
|
||||
|
||||
# Determine editable and viewable files based on extension
|
||||
editable_extensions = {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.log', '.sh', '.bat', '.ps1', '.php', '.rb', '.java', '.c', '.cpp', '.h', '.hpp'}
|
||||
viewable_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a'}
|
||||
for item in files:
|
||||
if not item['is_dir']:
|
||||
ext = Path(item['name']).suffix.lower()
|
||||
item['is_editable'] = ext in editable_extensions
|
||||
item['is_viewable'] = ext in viewable_extensions
|
||||
|
||||
success_message = self.request.query.get("success")
|
||||
error_message = self.request.query.get("error")
|
||||
|
||||
|
||||
@ -9,30 +9,44 @@ class UploadView(web.View):
|
||||
async def post(self):
|
||||
user_email = self.request["user"]["email"]
|
||||
file_service = self.request.app["file_service"]
|
||||
# Get current path from query parameter or form data
|
||||
current_path = self.request.query.get("current_path", "")
|
||||
current_path = ""
|
||||
|
||||
try:
|
||||
reader = await self.request.multipart()
|
||||
files_uploaded = []
|
||||
errors = []
|
||||
pending_files = []
|
||||
|
||||
while True:
|
||||
field = await reader.next()
|
||||
if field is None:
|
||||
break
|
||||
|
||||
# Check if the field is a file input
|
||||
if field.name == "file": # Assuming the input field name is 'file'
|
||||
if field.name == "current_path":
|
||||
current_path = (await field.read()).decode('utf-8').strip()
|
||||
print(f"Upload: current_path received: '{current_path}'")
|
||||
continue
|
||||
|
||||
if field.name == "file":
|
||||
filename = field.filename
|
||||
if not filename:
|
||||
errors.append("Filename is required for one of the files.")
|
||||
continue
|
||||
|
||||
content = await field.read()
|
||||
# Construct the full file path relative to the user's base directory
|
||||
full_file_path_for_service = f"{current_path}/{filename}" if current_path else filename
|
||||
pending_files.append((filename, content))
|
||||
|
||||
print(f"Upload: Processing {len(pending_files)} files to path: '{current_path}'")
|
||||
|
||||
for filename, content in pending_files:
|
||||
if current_path and not current_path.endswith('/'):
|
||||
full_file_path_for_service = f"{current_path}/{filename}"
|
||||
elif current_path:
|
||||
full_file_path_for_service = f"{current_path}{filename}"
|
||||
else:
|
||||
full_file_path_for_service = filename
|
||||
|
||||
print(f"Upload: Uploading file to: {full_file_path_for_service}")
|
||||
success = await file_service.upload_file(user_email, full_file_path_for_service, content)
|
||||
if success:
|
||||
files_uploaded.append(filename)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user