Update.
This commit is contained in:
parent
925f91a17c
commit
d8a419f528
@ -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(
|
||||||
|
|||||||
@ -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__":
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -1,3 +1,31 @@
|
|||||||
<footer aria-label="Site information and copyright">
|
<footer aria-label="Site information and copyright">
|
||||||
<p>© 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>© 2025 Retoors. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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")
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user