This commit is contained in:
retoor 2025-11-09 14:13:06 +01:00
parent 925f91a17c
commit d8a419f528
21 changed files with 1115 additions and 345 deletions

View File

@ -1,4 +1,6 @@
import aiosmtplib import aiosmtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.message import EmailMessage from email.message import EmailMessage
from aiohttp import web from aiohttp import web
import logging 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.") logger.error("SMTP host or sender email not configured. Cannot send email.")
return return
msg = EmailMessage() msg = MIMEMultipart('alternative')
msg["From"] = smtp_sender_email msg["From"] = smtp_sender_email
msg["To"] = recipient_email msg["To"] = recipient_email
msg["Subject"] = subject msg["Subject"] = subject
msg.set_content(body) html_part = MIMEText(body, 'html')
msg.attach(html_part)
try: try:
await aiosmtplib.send( await aiosmtplib.send(

View File

@ -1,3 +1,5 @@
import argparse
from aiohttp import web from aiohttp import web
import aiohttp_jinja2 import aiohttp_jinja2
import jinja2 import jinja2
@ -20,7 +22,7 @@ async def setup_services(app: web.Application):
data_path = base_path.parent / "data" data_path = base_path.parent / "data"
app["user_service"] = UserService(use_isolated_storage=True) app["user_service"] = UserService(use_isolated_storage=True)
app["config_service"] = ConfigService(data_path / "config.json") 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 # Setup aiojobs scheduler
app["scheduler"] = aiojobs.Scheduler() app["scheduler"] = aiojobs.Scheduler()
@ -67,9 +69,15 @@ def create_app():
return app return app
def main(): 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() app = create_app()
web.run_app(app) web.run_app(app, host=args.hostname, port=args.port)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -3,7 +3,8 @@ from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView
from .views.upload import UploadView from .views.upload import UploadView
from .views.migrate import MigrateView 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.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): def setup_routes(app):
app.router.add_view("/login", LoginView, name="login") 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("/order", OrderView, name="order")
app.router.add_view("/terms", SiteView, name="terms") app.router.add_view("/terms", SiteView, name="terms")
app.router.add_view("/privacy", SiteView, name="privacy") 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("/shared", SiteView, name="shared")
app.router.add_view("/recent", SiteView, name="recent") app.router.add_view("/recent", SiteView, name="recent")
app.router.add_view("/favorites", SiteView, name="favorites") 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_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}", 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_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 # Admin API routes for user and team management
app.router.add_get("/api/users", get_users, name="api_get_users") app.router.add_get("/api/users", get_users, name="api_get_users")

View File

@ -137,6 +137,63 @@ class FileService:
logger.info(f"download_file: Successfully read file: {file_path}") logger.info(f"download_file: Successfully read file: {file_path}")
return content, Path(file_path).name 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: async def delete_item(self, user_email: str, item_path: str) -> bool:
"""Deletes a file or folder for the user.""" """Deletes a file or folder for the user."""
metadata = await self._load_metadata(user_email) metadata = await self._load_metadata(user_email)

View File

@ -4,6 +4,7 @@ import hashlib
import aiofiles import aiofiles
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from .lock_manager import get_lock_manager
class StorageService: class StorageService:
@ -11,6 +12,7 @@ class StorageService:
def __init__(self, base_path: str = "data/user"): def __init__(self, base_path: str = "data/user"):
self.base_path = Path(base_path) self.base_path = Path(base_path)
self.base_path.mkdir(parents=True, exist_ok=True) self.base_path.mkdir(parents=True, exist_ok=True)
self.lock_manager = get_lock_manager()
def _hash(self, value: str) -> str: def _hash(self, value: str) -> str:
return hashlib.sha256(value.encode()).hexdigest() return hashlib.sha256(value.encode()).hexdigest()
@ -43,8 +45,10 @@ class StorageService:
file_path.parent.mkdir(parents=True, exist_ok=True) file_path.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(file_path, 'w') as f: lock = await self.lock_manager.get_lock(str(file_path))
await f.write(json.dumps(data, indent=2)) async with lock:
async with aiofiles.open(file_path, 'w') as f:
await f.write(json.dumps(data, indent=2))
return True return True
@ -58,14 +62,16 @@ class StorageService:
if not file_path.exists(): if not file_path.exists():
return None return None
async with aiofiles.open(file_path, 'r') as f: lock = await self.lock_manager.get_lock(str(file_path))
content = await f.read() async with lock:
if not content: async with aiofiles.open(file_path, 'r') as f:
return {} content = await f.read()
try: if not content:
return json.loads(content) return {}
except json.JSONDecodeError: try:
return {} return json.loads(content)
except json.JSONDecodeError:
return {}
async def delete(self, user_email: str, identifier: str) -> bool: async def delete(self, user_email: str, identifier: str) -> bool:
user_base = self._get_user_base_path(user_email) user_base = self._get_user_base_path(user_email)
@ -75,8 +81,11 @@ class StorageService:
raise ValueError("Invalid path: directory traversal detected") raise ValueError("Invalid path: directory traversal detected")
if file_path.exists(): if file_path.exists():
file_path.unlink() lock = await self.lock_manager.get_lock(str(file_path))
return True async with lock:
if file_path.exists():
file_path.unlink()
return True
return False return False
@ -98,9 +107,11 @@ class StorageService:
results = [] results = []
for json_file in user_base.rglob("*.json"): for json_file in user_base.rglob("*.json"):
if self._validate_path(json_file, user_base): if self._validate_path(json_file, user_base):
async with aiofiles.open(json_file, 'r') as f: lock = await self.lock_manager.get_lock(str(json_file))
content = await f.read() async with lock:
results.append(json.loads(content)) async with aiofiles.open(json_file, 'r') as f:
content = await f.read()
results.append(json.loads(content))
return results return results
@ -122,6 +133,7 @@ class UserStorageManager:
def __init__(self): def __init__(self):
self.storage = StorageService() self.storage = StorageService()
self.lock_manager = get_lock_manager()
async def save_user(self, user_email: str, user_data: Dict[str, Any]) -> bool: 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) return await self.storage.save(user_email, user_email, user_data)
@ -146,11 +158,13 @@ class UserStorageManager:
if user_dir.is_dir(): if user_dir.is_dir():
user_files = list(user_dir.rglob("*.json")) user_files = list(user_dir.rglob("*.json"))
for user_file in user_files: for user_file in user_files:
async with aiofiles.open(user_file, 'r') as f: lock = await self.lock_manager.get_lock(str(user_file))
content = await f.read() async with lock:
user_data = json.loads(content) async with aiofiles.open(user_file, 'r') as f:
if user_data.get('parent_email') == parent_email: content = await f.read()
all_users.append(user_data) user_data = json.loads(content)
if user_data.get('parent_email') == parent_email:
all_users.append(user_data)
return all_users return all_users
@ -165,8 +179,10 @@ class UserStorageManager:
if user_dir.is_dir(): if user_dir.is_dir():
user_files = list(user_dir.rglob("*.json")) user_files = list(user_dir.rglob("*.json"))
for user_file in user_files: for user_file in user_files:
async with aiofiles.open(user_file, 'r') as f: lock = await self.lock_manager.get_lock(str(user_file))
content = await f.read() async with lock:
all_users.append(json.loads(content)) async with aiofiles.open(user_file, 'r') as f:
content = await f.read()
all_users.append(json.loads(content))
return all_users return all_users

View File

@ -5,11 +5,13 @@ import bcrypt
import secrets import secrets
import datetime import datetime
from .storage_service import UserStorageManager from .storage_service import UserStorageManager
from .lock_manager import get_lock_manager
class UserService: class UserService:
def __init__(self, users_path: Path = None, use_isolated_storage: bool = True): def __init__(self, users_path: Path = None, use_isolated_storage: bool = True):
self.use_isolated_storage = use_isolated_storage self.use_isolated_storage = use_isolated_storage
self.lock_manager = get_lock_manager()
if use_isolated_storage: if use_isolated_storage:
self._storage_manager = UserStorageManager() self._storage_manager = UserStorageManager()
@ -27,11 +29,13 @@ class UserService:
except json.JSONDecodeError: except json.JSONDecodeError:
return [] return []
def _save_users(self): async def _save_users(self):
if self.use_isolated_storage: if self.use_isolated_storage:
return return
with open(self._users_path, "w") as f: lock = await self.lock_manager.get_lock(str(self._users_path))
json.dump(self._users, f, indent=4) async with lock:
with open(self._users_path, "w") as f:
json.dump(self._users, f, indent=4)
async def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]: async def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]:
if self.use_isolated_storage: if self.use_isolated_storage:
@ -80,7 +84,7 @@ class UserService:
await self._storage_manager.save_user(email, user) await self._storage_manager.save_user(email, user)
else: else:
self._users.append(user) self._users.append(user)
self._save_users() await self._save_users()
return user return user
@ -98,7 +102,7 @@ class UserService:
if self.use_isolated_storage: if self.use_isolated_storage:
await self._storage_manager.save_user(email, user) await self._storage_manager.save_user(email, user)
else: else:
self._save_users() await self._save_users()
return user return user
@ -109,7 +113,7 @@ class UserService:
initial_len = len(self._users) initial_len = len(self._users)
self._users = [user for user in self._users if user["email"] != email] self._users = [user for user in self._users if user["email"] != email]
if len(self._users) < initial_len: if len(self._users) < initial_len:
self._save_users() await self._save_users()
return True return True
return False return False
@ -126,7 +130,7 @@ class UserService:
self._users = [user for user in self._users if user.get("parent_email") != parent_email] self._users = [user for user in self._users if user.get("parent_email") != parent_email]
deleted_count = initial_len - len(self._users) deleted_count = initial_len - len(self._users)
if deleted_count > 0: if deleted_count > 0:
self._save_users() await self._save_users()
return deleted_count return deleted_count
@ -160,7 +164,7 @@ class UserService:
if self.use_isolated_storage: if self.use_isolated_storage:
await self._storage_manager.save_user(email, user) await self._storage_manager.save_user(email, user)
else: else:
self._save_users() await self._save_users()
return token return token
@ -192,7 +196,7 @@ class UserService:
if self.use_isolated_storage: if self.use_isolated_storage:
await self._storage_manager.save_user(email, user) await self._storage_manager.save_user(email, user)
else: else:
self._save_users() await self._save_users()
return True return True
@ -206,4 +210,4 @@ class UserService:
if self.use_isolated_storage: if self.use_isolated_storage:
await self._storage_manager.save_user(email, user) await self._storage_manager.save_user(email, user)
else: else:
self._save_users() await self._save_users()

View File

@ -1,64 +1,72 @@
:root { :root {
--primary-color: #4A90E2; /* Blue from image */ --primary-color: #0066FF; /* Vibrant blue */
--accent-color: #50E3C2; /* Greenish-blue from image */ --accent-color: #00D4FF; /* Bright cyan accent */
--secondary-color: #B8C2CC; /* Light grey-blue */ --secondary-color: #F7FAFC; /* Very light blue-grey */
--background-color: #F8F8F8; /* Very light grey background */ --background-color: #FFFFFF; /* Pure white background */
--text-color: #333333; /* Darker text */ --text-color: #1A202C; /* Dark blue-grey */
--light-text-color: #666666; /* Lighter text for descriptions */ --light-text-color: #718096; /* Medium grey for descriptions */
--border-color: #E0E0E0; /* Light grey border */ --border-color: #E2E8F0; /* Light grey border */
--card-background: #FFFFFF; /* White for cards */ --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-bg: var(--primary-color);
--btn-primary-text: #FFFFFF; --btn-primary-text: #FFFFFF;
--btn-primary-hover-bg: #3A7BD5; /* Slightly darker blue */ --btn-primary-hover-bg: #0052CC; /* Darker blue */
--btn-secondary-bg: #E0E0E0; /* Light grey for secondary buttons */ --btn-secondary-bg: #EDF2F7; /* Light grey */
--btn-secondary-text: var(--text-color); --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-border: var(--primary-color);
--btn-outline-text: 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 { html, body {
height: 100%; height: 100%;
margin: 0; 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); background-color: var(--background-color);
color: var(--text-color); color: var(--text-color);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
line-height: 1.6; /* Improve readability */ line-height: 1.6;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
font-size: 16px;
} }
/* General typography */ /* General typography */
h1 { h1 {
font-size: 2.2rem; /* Slightly larger heading */ font-size: 3.5rem;
font-weight: 700; font-weight: 700;
margin-bottom: 1rem; margin-bottom: 1.5rem;
color: var(--text-color); color: var(--text-color);
line-height: 1.2;
letter-spacing: -0.02em;
} }
h2 { h2 {
font-size: 2.2rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
margin-bottom: 0.8rem; margin-bottom: 1rem;
color: var(--text-color); color: var(--text-color);
line-height: 1.3;
letter-spacing: -0.01em;
} }
h3 { h3 {
font-size: 1.8rem; font-size: 1.5rem;
font-weight: 500; font-weight: 600;
margin-bottom: 0.6rem; margin-bottom: 0.75rem;
color: var(--text-color); color: var(--text-color);
line-height: 1.4;
} }
p { p {
margin-bottom: 1rem; margin-bottom: 1rem;
color: var(--light-text-color); color: var(--light-text-color);
font-size: 1.125rem;
line-height: 1.7;
} }
/* Card-like styling for sections */ /* Card-like styling for sections */
@ -147,96 +155,100 @@ p {
} }
/* Header and Navigation */ /* Header and Navigation */
header { .site-header {
background-color: var(--card-background); /* White background for header */ background-color: #FFFFFF;
box-shadow: 0 2px 4px var(--shadow-color); border-bottom: 1px solid var(--border-color);
padding: 15px 20px;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1000; z-index: 1000;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
} }
nav { .site-nav {
padding: 0 20px;
}
.nav-container {
display: flex; display: flex;
flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
min-height: 70px;
gap: 2rem;
} }
nav .logo { .brand {
text-decoration: none;
display: flex; display: flex;
align-items: center; align-items: center;
text-decoration: none; flex-shrink: 0;
color: var(--text-color); }
.brand-text {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: var(--text-color);
letter-spacing: -0.01em;
} }
nav .logo img { .nav-menu {
height: 30px; /* Adjust logo size */
margin-right: 10px;
}
nav .nav-links {
display: flex; display: flex;
flex-direction: row;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 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); color: var(--text-color);
text-decoration: none; text-decoration: none;
font-weight: 500; 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); color: var(--primary-color);
} }
.btn-primary-nav { .nav-actions {
background-color: var(--primary-color); display: flex;
color: white; flex-direction: row;
padding: 10px 20px; align-items: center;
border-radius: 5px; gap: 1rem;
flex-shrink: 0;
}
.nav-link {
color: var(--text-color);
text-decoration: none; text-decoration: none;
font-weight: 500; font-weight: 500;
transition: background-color 0.3s ease; font-size: 1rem;
transition: color 0.2s ease;
white-space: nowrap;
} }
.btn-primary-nav:hover { .nav-link:hover {
background-color: var(--btn-primary-hover-bg);
}
.btn-outline-nav {
background-color: transparent;
color: var(--primary-color); 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 content area */
main { main {
flex-grow: 1; /* Allow main content to take available space */ flex-grow: 1;
width: 100%; width: 100%;
max-width: 1200px; /* Match header max-width */ margin: 0;
margin: 0 auto; /* Center the main content */ padding: 0;
padding: 40px 20px; /* Add padding */
box-sizing: border-box; box-sizing: border-box;
background-color: var(--background-color); /* Light background for main content */ background-color: var(--background-color);
} }
/* Form specific styles */ /* Form specific styles */
@ -244,17 +256,17 @@ main {
max-width: 450px; max-width: 450px;
margin: 50px auto; margin: 50px auto;
background-color: var(--card-background); background-color: var(--card-background);
border-radius: 12px; /* Slightly more rounded corners */ border-radius: 12px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); /* Softer, more prominent shadow */ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: 3rem; /* Increased padding */ padding: 2.5rem;
text-align: left; text-align: left;
} }
.form-container h2 { .form-container h2 {
text-align: center; text-align: center;
margin-bottom: 35px; /* Increased margin */ margin-bottom: 2rem;
color: var(--text-color); color: var(--text-color);
font-size: 2.2rem; /* Slightly larger heading */ font-size: 2rem;
font-weight: 700; font-weight: 700;
} }
@ -309,16 +321,17 @@ main {
.btn-small, .btn-small,
.btn-danger { .btn-danger {
display: inline-block; display: inline-block;
padding: 0.6rem 1.2rem; padding: 0.65rem 1.5rem;
font-size: 0.9rem; font-size: 0.95rem;
font-weight: 500; font-weight: 600;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
border: 1px solid transparent; border: 2px solid transparent;
box-sizing: border-box; box-sizing: border-box;
white-space: nowrap;
} }
.btn-primary { .btn-primary {
@ -330,6 +343,8 @@ main {
.btn-primary:hover { .btn-primary:hover {
background-color: var(--btn-primary-hover-bg); background-color: var(--btn-primary-hover-bg);
border-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 { .btn-outline {
@ -342,6 +357,7 @@ main {
background-color: var(--btn-outline-hover-bg); background-color: var(--btn-outline-hover-bg);
color: var(--btn-outline-text); color: var(--btn-outline-text);
border-color: var(--btn-outline-border); border-color: var(--btn-outline-border);
transform: translateY(-1px);
} }
.btn-small { .btn-small {
@ -372,28 +388,81 @@ main {
text-align: center; text-align: center;
} }
/* Responsive adjustments for base.css */ /* Responsive adjustments */
@media (max-width: 992px) {
.nav-menu {
gap: 1.25rem;
}
.nav-container {
gap: 1rem;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
nav { .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; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 15px; gap: 1rem;
} order: 2;
nav .logo {
width: 100%;
justify-content: center;
margin-bottom: 10px; margin-bottom: 10px;
} }
nav .nav-links {
width: 100%; .nav-menu a {
justify-content: center; font-size: 0.9rem;
margin-bottom: 10px;
} }
.btn-primary-nav {
.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%; width: 100%;
}
.nav-actions .btn-primary,
.nav-actions .btn-outline {
width: 100%;
max-width: 280px;
text-align: center; text-align: center;
} }
main {
padding: 20px 15px;
}
} }

View File

@ -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 */ /* General styles for hero sections on content pages */
.hero-intro { .hero-intro {
padding: 20px; padding: 20px;

View File

@ -10,15 +10,25 @@
} }
.dashboard-sidebar { .dashboard-sidebar {
flex: 0 0 250px; /* Fixed width sidebar */ flex: 0 0 250px;
background-color: var(--card-background); background-color: var(--card-background);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color); box-shadow: 0 4px 12px var(--shadow-color);
padding: 20px; padding: 20px;
position: sticky; /* Make sidebar sticky */ position: sticky;
top: 100px; /* Adjust based on header height */ top: 100px;
max-height: calc(100vh - 120px); /* Adjust based on header/footer */ max-height: calc(100vh - 120px);
overflow-y: auto; overflow-y: hidden;
overflow-x: hidden;
}
.dashboard-sidebar::-webkit-scrollbar {
display: none;
}
.dashboard-sidebar {
-ms-overflow-style: none;
scrollbar-width: none;
} }
.sidebar-menu ul { .sidebar-menu ul {
@ -162,6 +172,7 @@
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-top: 20px; margin-top: 20px;
table-layout: fixed;
} }
.file-list-table th, .file-list-table th,
@ -181,6 +192,9 @@
.file-list-table td { .file-list-table td {
color: var(--light-text-color); color: var(--light-text-color);
word-break: break-all;
overflow-wrap: break-word;
max-width: 300px;
} }
.file-list-table tr:last-child td { .file-list-table tr:last-child td {
@ -200,6 +214,43 @@
color: var(--light-text-color); 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 */ /* Responsive adjustments */
@media (max-width: 992px) { @media (max-width: 992px) {
.dashboard-layout { .dashboard-layout {
@ -249,4 +300,8 @@
padding: 10px 12px; padding: 10px 12px;
font-size: 0.85rem; font-size: 0.85rem;
} }
.file-list-table td {
max-width: 150px;
}
} }

View File

@ -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 { .modal {
display: none; display: none !important;
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;
left: 0; left: 0;
@ -10,6 +25,10 @@
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
} }
.modal.show {
display: block !important;
}
.modal-content { .modal-content {
background-color: var(--card-background); background-color: var(--card-background);
margin: 10% auto; margin: 10% auto;
@ -161,6 +180,7 @@
.file-list-table table { .file-list-table table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed;
} }
.file-list-table th { .file-list-table th {
@ -177,6 +197,9 @@
padding: 12px 15px; padding: 12px 15px;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
color: var(--text-color); color: var(--text-color);
word-break: break-all;
overflow-wrap: break-word;
max-width: 300px;
} }
.file-list-table tr:hover { .file-list-table tr:hover {
@ -197,12 +220,46 @@
.file-list-table a { .file-list-table a {
color: var(--accent-color); color: var(--accent-color);
text-decoration: none; text-decoration: none;
word-break: break-all;
overflow-wrap: break-word;
} }
.file-list-table a:hover { .file-list-table a:hover {
text-decoration: underline; 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) { @media (max-width: 768px) {
.modal-content { .modal-content {
width: 95%; width: 95%;
@ -227,6 +284,10 @@
padding: 8px 10px; padding: 8px 10px;
} }
.file-list-table td {
max-width: 150px;
}
.dashboard-actions { .dashboard-actions {
flex-wrap: wrap; flex-wrap: wrap;
} }

View File

@ -1,14 +1,70 @@
footer { footer {
background-color: var(--card-background); /* Consistent with header */ background-color: var(--card-background);
color: var(--light-text-color); /* Subtle text color */ color: var(--light-text-color);
padding: 1.5rem 2rem; padding: 2rem 2rem 1rem 2rem;
text-align: center; border-top: 1px solid var(--border-color);
border-top: 1px solid var(--border-color); /* Separator from content */ margin-top: auto;
margin-top: auto; /* Push footer to the bottom */ box-shadow: 0 -2px 4px var(--shadow-color);
box-shadow: 0 -2px 4px var(--shadow-color); /* Subtle shadow upwards */
} }
footer p { .footer-content {
margin: 0; /* Remove default paragraph margin */ display: flex;
font-size: 0.9rem; 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;
}
} }

View File

@ -1,135 +1,260 @@
/* Styles for the Homepage (index.html) */ /* Hero Section */
.hero-section { .hero-section {
text-align: center; text-align: center;
padding: 60px 20px; padding: 100px 20px 80px;
/* Removed background-color and box-shadow to match image */ background: linear-gradient(135deg, #F7FAFC 0%, #FFFFFF 100%);
margin-bottom: 40px; margin-bottom: 0;
position: relative; }
overflow: hidden;
.hero-content {
max-width: 900px;
margin: 0 auto;
} }
.hero-section h1 { .hero-section h1 {
font-size: 3.5rem; font-size: 3.75rem;
font-weight: 700; font-weight: 700;
color: var(--text-color); color: var(--text-color);
margin-bottom: 0.5rem; /* Adjusted margin to match image */ margin-bottom: 1.5rem;
line-height: 1.2; line-height: 1.15;
letter-spacing: -0.03em;
} }
.hero-section p { .hero-subtitle {
font-size: 1.3rem; font-size: 1.25rem;
color: var(--light-text-color); color: var(--light-text-color);
max-width: 800px; max-width: 700px;
margin: 0 auto 2.5rem auto; margin: 0 auto 2.5rem;
line-height: 1.5; line-height: 1.6;
} }
.benefits-grid { .hero-ctas {
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;
display: flex; display: flex;
flex-direction: column; gap: 1rem;
align-items: center;
justify-content: center; justify-content: center;
min-height: 250px; /* Ensure cards have a consistent height */ margin-bottom: 1.5rem;
} }
.benefit-card:hover { .hero-btn {
transform: translateY(-5px); padding: 1rem 2rem;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12); font-size: 1.125rem;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
} }
.benefit-card img.icon { .hero-subtext {
width: 60px; font-size: 0.875rem;
height: 60px; color: var(--light-text-color);
margin-bottom: 20px; margin: 0;
/* Icons in the image are colored, not inheriting text color */
} }
.benefit-card h3 { /* Features Section */
font-size: 1.5rem; .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); color: var(--text-color);
margin-bottom: 15px;
} }
.benefit-card p { .feature-card p {
font-size: 1rem; font-size: 1rem;
color: var(--light-text-color); color: var(--light-text-color);
line-height: 1.6; line-height: 1.6;
} }
/* Specific card colors from the image */ /* Use Cases Section */
.family-card { .use-cases-section {
background-color: #D0E6F0; /* Light blue */ padding: 80px 20px;
background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%);
text-align: center;
} }
.professional-card { .use-cases-section h2 {
background-color: #F0E0D0; /* Light orange */ font-size: 2.5rem;
margin-bottom: 3rem;
color: var(--text-color);
} }
.student-card { .use-cases-grid {
background-color: #D0F0D0; /* Light green */ display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
} }
.find-plan-btn { .use-case-card {
margin-top: 40px; background-color: #FFFFFF;
padding: 15px 30px; padding: 2.5rem;
font-size: 1.1rem; 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; font-weight: 600;
border-radius: 5px; margin-bottom: 1rem;
background-color: #4A90E2; /* Blue from the image */ 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; 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; text-decoration: none;
display: inline-block; display: inline-block;
transition: background-color 0.3s ease; transition: transform 0.3s ease, box-shadow 0.3s ease;
} }
.find-plan-btn:hover { .cta-btn:hover {
background-color: #3A7BD5; /* Darker blue on 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) { @media (max-width: 992px) {
.hero-section h1 { .hero-section h1 {
font-size: 3rem; 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) { @media (max-width: 768px) {
.hero-section { .hero-section {
padding: 40px 15px; padding: 60px 20px 50px;
} }
.hero-section h1 { .hero-section h1 {
font-size: 2.5rem; 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; .hero-btn {
}
.find-plan-btn {
width: 100%; 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 { .hero-section h1 {
font-size: 2rem; 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%; .features-header h2,
box-sizing: border-box; .use-cases-section h2,
.cta-section h2 {
font-size: 1.75rem;
} }
} }

View File

@ -1,9 +1,11 @@
export function showUploadModal() { export function showUploadModal() {
document.getElementById('upload-modal').style.display = 'block'; const modal = document.getElementById('upload-modal');
// Clear previous selections and progress if (modal) {
modal.classList.add('show');
}
document.getElementById('selected-files-preview').innerHTML = ''; document.getElementById('selected-files-preview').innerHTML = '';
document.getElementById('upload-progress-container').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; document.getElementById('start-upload-btn').disabled = true;
} }
@ -13,6 +15,11 @@ document.addEventListener('DOMContentLoaded', () => {
const startUploadBtn = document.getElementById('start-upload-btn'); const startUploadBtn = document.getElementById('start-upload-btn');
const uploadProgressContainer = document.getElementById('upload-progress-container'); const uploadProgressContainer = document.getElementById('upload-progress-container');
if (!fileInput || !selectedFilesPreview || !startUploadBtn || !uploadProgressContainer) {
console.error('Upload elements not found');
return;
}
let filesToUpload = []; let filesToUpload = [];
fileInput.addEventListener('change', (event) => { fileInput.addEventListener('change', (event) => {
@ -55,62 +62,94 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
async function uploadFiles(files) { async function uploadFiles(files) {
startUploadBtn.disabled = true; // Disable button during upload startUploadBtn.disabled = true;
uploadProgressContainer.innerHTML = ''; // Clear previous progress uploadProgressContainer.innerHTML = '';
const currentPath = new URLSearchParams(window.location.search).get('path') || ''; const currentPath = new URLSearchParams(window.location.search).get('path') || '';
console.log('Uploading to directory:', currentPath || '(root)');
for (const file of files) { let completedUploads = 0;
const formData = new FormData(); let totalFiles = files.length;
formData.append('file', file); let hasErrors = false;
const progressBarContainer = document.createElement('div'); const uploadPromises = files.map(file => {
progressBarContainer.className = 'progress-bar-container'; return new Promise((resolve, reject) => {
progressBarContainer.innerHTML = ` const formData = new FormData();
<div class="file-name">${file.name}</div> formData.append('current_path', currentPath);
<div class="progress-bar-wrapper"> formData.append('file', file);
<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>
`;
uploadProgressContainer.appendChild(progressBarContainer);
const xhr = new XMLHttpRequest(); const progressBarContainer = document.createElement('div');
xhr.open('POST', `/files/upload?current_path=${encodeURIComponent(currentPath)}`, true); progressBarContainer.className = 'progress-bar-container';
progressBarContainer.innerHTML = `
<div class="file-name">${file.name}</div>
<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>
`;
uploadProgressContainer.appendChild(progressBarContainer);
xhr.upload.addEventListener('progress', (event) => { const xhr = new XMLHttpRequest();
if (event.lengthComputable) { xhr.open('POST', `/files/upload`, true);
const percent = (event.loaded / event.total) * 100;
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `${percent}%`; xhr.upload.addEventListener('progress', (event) => {
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `${Math.round(percent)}%`; if (event.lengthComputable) {
} const percent = (event.loaded / event.total) * 100;
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.`);
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}`);
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}.`);
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);
}); });
});
xhr.addEventListener('load', () => { try {
if (xhr.status === 200) { await Promise.allSettled(uploadPromises);
console.log(`File ${file.name} uploaded successfully.`);
// Update progress to 100% on completion setTimeout(() => {
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `100%`; const currentUrl = new URL(window.location.href);
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `100% (Done)`; const pathParam = currentUrl.searchParams.get('path');
if (pathParam) {
window.location.href = `/files?path=${encodeURIComponent(pathParam)}`;
} else { } else {
console.error(`Error uploading ${file.name}: ${xhr.statusText}`); window.location.href = '/files';
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Failed (${xhr.status})`;
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
} }
}); }, 500);
} catch (error) {
xhr.addEventListener('error', () => { console.error('Error during upload:', error);
console.error(`Network error uploading ${file.name}.`); startUploadBtn.disabled = false;
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Network Error`;
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
});
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
setTimeout(() => {
window.location.reload();
}, 1000);
} }
}); });

View File

@ -88,17 +88,23 @@ document.addEventListener('DOMContentLoaded', () => {
// Helper functions for modals // Helper functions for modals
function showNewFolderModal() { 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) { 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) { window.onclick = function(event) {
if (event.target.classList.contains('modal')) { 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 loading = document.getElementById('share-loading');
const shareLinkInput = document.getElementById('share-link-input'); const shareLinkInput = document.getElementById('share-link-input');
const shareFileName = document.getElementById('share-file-name'); 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 = ''; shareLinkInput.value = '';
if (shareLinksList) shareLinksList.innerHTML = ''; if (shareLinksList) shareLinksList.innerHTML = '';
linkContainer.style.display = 'none'; linkContainer.style.display = 'none';
loading.style.display = 'block'; loading.style.display = 'block';
modal.style.display = 'block'; modal.classList.add('show');
if (paths.length === 1) { if (paths.length === 1) {
shareFileName.textContent = `Sharing: ${names[0]}`; shareFileName.textContent = `Sharing: ${names[0]}`;
@ -180,7 +185,6 @@ document.addEventListener('DOMContentLoaded', () => {
const deleteMessage = document.getElementById('delete-message'); const deleteMessage = document.getElementById('delete-message');
const deleteModal = document.getElementById('delete-modal'); const deleteModal = document.getElementById('delete-modal');
// Clear previous hidden inputs
deleteForm.querySelectorAll('input[name="paths[]"]').forEach(input => input.remove()); deleteForm.querySelectorAll('input[name="paths[]"]').forEach(input => input.remove());
if (Array.isArray(paths) && paths.length > 1) { 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.`; deleteMessage.textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
deleteForm.action = `/files/delete/${path}`; deleteForm.action = `/files/delete/${path}`;
} }
deleteModal.style.display = 'block'; deleteModal.classList.add('show');
} }
// Selection and action buttons // Selection and action buttons

View File

@ -1,3 +1,31 @@
<footer aria-label="Site information and copyright"> <footer aria-label="Site information and copyright">
<p>&copy; 2025 Retoors. All rights reserved.</p> <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>&copy; 2025 Retoors. All rights reserved.</p>
</div>
</footer> </footer>

View File

@ -1,22 +1,26 @@
<header> <header class="site-header">
<nav aria-label="Main navigation"> <nav class="site-nav" aria-label="Main navigation">
<a href="/" class="logo" aria-label="HomeBase Storage home"> <div class="nav-container">
<img src="/static/images/retoors-logo.svg" alt="HomeBase Storage" /> <a href="/" class="brand" aria-label="Retoor's Cloud Solutions home">
<span>HomeBase Storage</span> <span class="brand-text">Retoor's</span>
</a> </a>
<ul class="nav-links"> <ul class="nav-menu">
<li><a href="/solutions" aria-label="Our Solutions">Solutions</a></li> <li><a href="/solutions">Solutions</a></li>
<li><a href="/pricing" aria-label="Pricing Plans">Pricing</a></li> <li><a href="/pricing">Pricing</a></li>
<li><a href="/security" aria-label="Security Information">Security</a></li> <li><a href="/security">Security</a></li>
<li><a href="/support" aria-label="Support Page">Support</a></li> {% if request['user'] %}
{% if request['user'] %} <li><a href="/files">My Files</a></li>
<li><a href="/dashboard" aria-label="User Dashboard">Dashboard</a></li> {% endif %}
<li><a href="/files" aria-label="File Browser">File Browser</a></li> </ul>
<li><a href="/logout" class="btn-primary-nav" aria-label="Logout">Logout</a></li> <div class="nav-actions">
{% else %} {% if request['user'] %}
<li><a href="/login" class="btn-outline-nav" aria-label="Sign In to your account">Sign In</a></li> <a href="/files" class="nav-link">Dashboard</a>
<li><a href="/register" class="btn-primary-nav" aria-label="Start your free trial">Start Your Free Trial</a></li> <a href="/logout" class="btn-outline">Logout</a>
{% endif %} {% else %}
</ul> <a href="/login" class="nav-link">Sign In</a>
<a href="/register" class="btn-primary">Get Started Free</a>
{% endif %}
</div>
</div>
</nav> </nav>
</header> </header>

View File

@ -3,18 +3,17 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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/base.css">
<link rel="stylesheet" href="/static/css/components/footer.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 %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
{% include 'components/navigation.html' %}
<div class="container"> {% block content %}{% endblock %}
{% block content %}{% endblock %}
</div>
{% include 'components/footer.html' %} {% include 'components/footer.html' %}
{% include 'components/cookie_banner.html' %}
<script src="/static/js/main.js" type="module"></script> <script src="/static/js/main.js" type="module"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>

View File

@ -1,12 +1,12 @@
{% extends "layouts/dashboard.html" %} {% 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 %} {% block dashboard_head %}
<link rel="stylesheet" href="/static/css/components/file_browser.css"> <link rel="stylesheet" href="/static/css/components/file_browser.css">
{% endblock %} {% 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 %} {% block dashboard_actions %}
<button class="btn-primary" id="new-folder-btn">+ New</button> <button class="btn-primary" id="new-folder-btn">+ New</button>
@ -55,7 +55,13 @@
<a href="/files?path={{ item.path }}">{{ item.name }}</a> <a href="/files?path={{ item.path }}">{{ item.name }}</a>
{% else %} {% else %}
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon"> <img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
{{ item.name }} {% 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 %} {% endif %}
</td> </td>
<td>{{ user.email }}</td> <td>{{ user.email }}</td>

View File

@ -1,6 +1,6 @@
{% extends "layouts/base.html" %} {% 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 %} {% block head %}
<link rel="stylesheet" href="/static/css/components/index.css"> <link rel="stylesheet" href="/static/css/components/index.css">
@ -9,26 +9,114 @@
{% block content %} {% block content %}
<main> <main>
<section class="hero-section"> <section class="hero-section">
<h1>Solutions for Everyone</h1> <div class="hero-content">
<p>Solutions for Everyone</p> <h1>Your files, safe and accessible everywhere</h1>
<div class="benefits-grid"> <p class="hero-subtitle">Store, sync, and share your files with enterprise-grade security. Access from any device, anytime, anywhere.</p>
<div class="benefit-card family-card"> <div class="hero-ctas">
<img src="/static/images/icon-families.svg" alt="Families Icon" class="icon"> <a href="/register" class="btn-primary hero-btn">Get Started Free</a>
<h3>For Families</h3> <a href="/pricing" class="btn-outline hero-btn">View Pricing</a>
<p>Securely backup and share precious photos and videos. Keep fond memories safe for generations.</p>
</div> </div>
<div class="benefit-card professional-card"> <p class="hero-subtext">No credit card required • 10 GB free storage</p>
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="icon"> </div>
<h3>For Professionals</h3> </section>
<p>Organize important work documents, collaborate in teams, and access files from anywhere.</p>
<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>
<div class="benefit-card student-card"> <div class="feature-card">
<img src="/static/images/icon-students.svg" alt="Students Icon" class="icon"> <div class="feature-icon">
<h3>For Students</h3> <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<p>Store projects, notes, research papers. Access study materials across your devices.</p> <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>
</div> </div>
<a href="/solutions" class="btn-primary find-plan-btn">Find Your Perfect Plan</a> </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>Keep precious memories safe. Share photos and videos with family members securely.</p>
</div>
<div class="use-case-card">
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="use-case-icon">
<h3>For Professionals</h3>
<p>Collaborate on projects, share documents, and work from anywhere with confidence.</p>
</div>
<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 all your coursework, projects, and study materials in one secure place.</p>
</div>
</div>
</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> </section>
</main> </main>
{% endblock %} {% endblock %}

View File

@ -38,6 +38,18 @@ class SiteView(web.View):
return await self.terms() return await self.terms()
elif self.request.path == "/privacy": elif self.request.path == "/privacy":
return await self.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": elif self.request.path == "/shared":
return await self.shared() return await self.shared()
elif self.request.path == "/recent": 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")} "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 @login_required
async def shared(self): async def shared(self):
return aiohttp_jinja2.render_template( return aiohttp_jinja2.render_template(
@ -141,6 +183,15 @@ class FileBrowserView(web.View):
path = self.request.query.get("path", "") path = self.request.query.get("path", "")
files = await file_service.list_files(user_email, 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") success_message = self.request.query.get("success")
error_message = self.request.query.get("error") error_message = self.request.query.get("error")

View File

@ -9,35 +9,49 @@ class UploadView(web.View):
async def post(self): async def post(self):
user_email = self.request["user"]["email"] user_email = self.request["user"]["email"]
file_service = self.request.app["file_service"] file_service = self.request.app["file_service"]
# Get current path from query parameter or form data current_path = ""
current_path = self.request.query.get("current_path", "")
try: try:
reader = await self.request.multipart() reader = await self.request.multipart()
files_uploaded = [] files_uploaded = []
errors = [] errors = []
pending_files = []
while True: while True:
field = await reader.next() field = await reader.next()
if field is None: if field is None:
break break
# Check if the field is a file input if field.name == "current_path":
if field.name == "file": # Assuming the input field name is 'file' 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 filename = field.filename
if not filename: if not filename:
errors.append("Filename is required for one of the files.") errors.append("Filename is required for one of the files.")
continue continue
content = await field.read() content = await field.read()
# Construct the full file path relative to the user's base directory pending_files.append((filename, content))
full_file_path_for_service = f"{current_path}/{filename}" if current_path else filename
print(f"Upload: Processing {len(pending_files)} files to path: '{current_path}'")
success = await file_service.upload_file(user_email, full_file_path_for_service, content)
if success: for filename, content in pending_files:
files_uploaded.append(filename) if current_path and not current_path.endswith('/'):
else: full_file_path_for_service = f"{current_path}/{filename}"
errors.append(f"Failed to upload file '{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)
else:
errors.append(f"Failed to upload file '{filename}'")
if errors: if errors:
return json_response({"status": "error", "message": "Some files failed to upload", "details": errors}, status=500) return json_response({"status": "error", "message": "Some files failed to upload", "details": errors}, status=500)