This commit is contained in:
retoor 2025-11-09 19:09:51 +01:00
parent 155992f196
commit c281f1e9ea
15 changed files with 774 additions and 163 deletions

View File

@ -5,6 +5,16 @@ 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.editor import FileEditorView, FileContentView
from .views.viewer import ViewerView from .views.viewer import ViewerView
from .views.sharing import (
create_share_page,
create_share_handler,
view_share,
download_shared_file,
manage_shares,
get_share_details,
update_share,
get_item_shares
)
def setup_routes(app): def setup_routes(app):
app.router.add_view("/login", LoginView, name="login") app.router.add_view("/login", LoginView, name="login")
@ -56,3 +66,12 @@ def setup_routes(app):
app.router.add_delete("/api/users/{email}", delete_user, name="api_delete_user") app.router.add_delete("/api/users/{email}", delete_user, name="api_delete_user")
app.router.add_get("/api/users/{email}", get_user_details, name="api_get_user_details") app.router.add_get("/api/users/{email}", get_user_details, name="api_get_user_details")
app.router.add_delete("/api/teams/{parent_email}", delete_team, name="api_delete_team") app.router.add_delete("/api/teams/{parent_email}", delete_team, name="api_delete_team")
app.router.add_get("/sharing/create", create_share_page, name="create_share_page")
app.router.add_post("/api/sharing/create", create_share_handler, name="create_share")
app.router.add_get("/share/{share_id}", view_share, name="view_share")
app.router.add_get("/share/{share_id}/download", download_shared_file, name="download_share")
app.router.add_get("/sharing/manage", manage_shares, name="manage_shares")
app.router.add_get("/api/sharing/{share_id}", get_share_details, name="get_share_details")
app.router.add_put("/api/sharing/{share_id}", update_share, name="update_share")
app.router.add_get("/api/sharing/item/shares", get_item_shares, name="get_item_shares")

View File

@ -8,6 +8,7 @@ import logging
import hashlib import hashlib
import os import os
from .storage_service import StorageService from .storage_service import StorageService
from .sharing_service import SharingService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@ -21,6 +22,7 @@ class FileService:
self.base_dir = base_dir self.base_dir = base_dir
self.user_service = user_service self.user_service = user_service
self.storage = StorageService() self.storage = StorageService()
self.sharing_service = SharingService()
self.base_dir.mkdir(parents=True, exist_ok=True) self.base_dir.mkdir(parents=True, exist_ok=True)
self.drives_dir = self.base_dir / "drives" self.drives_dir = self.base_dir / "drives"
self.drives_dir.mkdir(exist_ok=True) self.drives_dir.mkdir(exist_ok=True)
@ -225,77 +227,92 @@ class FileService:
logger.info(f"delete_item: Item deleted: {item_path}") logger.info(f"delete_item: Item deleted: {item_path}")
return True return True
async def generate_share_link(self, user_email: str, item_path: str) -> str | None: async def generate_share_link(
"""Generates a shareable link for a file or folder.""" self,
user_email: str,
item_path: str,
permission: str = "view",
scope: str = "public",
password: str = None,
expiration_days: int = None,
disable_download: bool = False,
recipient_emails: list = None
) -> str | None:
logger.debug(f"generate_share_link: Generating link for user '{user_email}', item '{item_path}'") logger.debug(f"generate_share_link: Generating link for user '{user_email}', item '{item_path}'")
metadata = await self._load_metadata(user_email) metadata = await self._load_metadata(user_email)
if item_path not in metadata: if item_path not in metadata:
logger.warning(f"generate_share_link: Item does not exist: {item_path}") logger.warning(f"generate_share_link: Item does not exist: {item_path}")
return None return None
user = await self.user_service.get_user_by_email(user_email)
if not user:
logger.warning(f"generate_share_link: User not found: {user_email}")
return None
share_id = str(uuid.uuid4()) share_id = await self.sharing_service.create_share(
if "shared_items" not in user: owner_email=user_email,
user["shared_items"] = {} item_path=item_path,
user["shared_items"][share_id] = { permission=permission,
"user_email": user_email, scope=scope,
"item_path": item_path, password=password,
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), expiration_days=expiration_days,
"expires_at": (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=7)).isoformat(), # 7-day expiry disable_download=disable_download,
} recipient_emails=recipient_emails
await self.user_service.update_user(user_email, shared_items=user["shared_items"]) )
logger.info(f"generate_share_link: Share link generated with ID: {share_id} for item: {item_path}") logger.info(f"generate_share_link: Share link generated with ID: {share_id} for item: {item_path}")
return share_id return share_id
async def get_shared_item(self, share_id: str) -> dict | None: async def get_shared_item(self, share_id: str, password: str = None, accessor_email: str = None) -> dict | None:
"""Retrieves information about a shared item."""
logger.debug(f"get_shared_item: Retrieving shared item with ID: {share_id}") logger.debug(f"get_shared_item: Retrieving shared item with ID: {share_id}")
all_users = await self.user_service.get_all_users()
for user in all_users: share = await self.sharing_service.get_share(share_id)
if "shared_items" in user and share_id in user["shared_items"]: if not share:
shared_item = user["shared_items"][share_id] logger.warning(f"get_shared_item: No valid shared item found for ID: {share_id}")
expiry_time = datetime.datetime.fromisoformat(shared_item["expires_at"]) return None
if expiry_time > datetime.datetime.now(datetime.timezone.utc):
logger.info(f"get_shared_item: Found valid shared item for ID: {share_id}") if not await self.sharing_service.verify_share_access(share_id, password, accessor_email):
return shared_item logger.warning(f"get_shared_item: Access denied for share {share_id}")
else: return None
logger.warning(f"get_shared_item: Shared item {share_id} has expired.")
logger.warning(f"get_shared_item: No valid shared item found for ID: {share_id}") await self.sharing_service.record_share_access(share_id, accessor_email)
return None
logger.info(f"get_shared_item: Found valid shared item for ID: {share_id}")
return share
async def get_shared_file_content(self, share_id: str, password: str = None, accessor_email: str = None, requested_file_path: str = None) -> tuple[bytes, str] | None:
async def get_shared_file_content(self, share_id: str, requested_file_path: str | None = None) -> tuple[bytes, str] | None:
"""Retrieves the content of a shared file."""
logger.debug(f"get_shared_file_content: Retrieving content for shared file with ID: {share_id}, requested_file_path: {requested_file_path}") logger.debug(f"get_shared_file_content: Retrieving content for shared file with ID: {share_id}, requested_file_path: {requested_file_path}")
shared_item = await self.get_shared_item(share_id)
shared_item = await self.get_shared_item(share_id, password, accessor_email)
if not shared_item: if not shared_item:
return None return None
user_email = shared_item["user_email"] if shared_item.get("disable_download", False):
item_path = shared_item["item_path"] # This is the path of the originally shared item (file or folder) logger.warning(f"get_shared_file_content: Download disabled for share {share_id}")
return None
user_email = shared_item["owner_email"]
item_path = shared_item["item_path"]
target_path = item_path target_path = item_path
if requested_file_path: if requested_file_path:
target_path = requested_file_path target_path = requested_file_path
# Security check: Ensure the requested file is actually within the shared item's directory if not target_path.startswith(item_path + '/') and target_path != item_path:
if not target_path.startswith(item_path + '/'):
logger.warning(f"get_shared_file_content: Requested file path '{requested_file_path}' is not within shared item path '{item_path}' for share_id: {share_id}") logger.warning(f"get_shared_file_content: Requested file path '{requested_file_path}' is not within shared item path '{item_path}' for share_id: {share_id}")
return None return None
return await self.download_file(user_email, target_path) return await self.download_file(user_email, target_path)
async def get_shared_folder_content(self, share_id: str) -> list | None: async def get_shared_folder_content(self, share_id: str, password: str = None, accessor_email: str = None) -> list | None:
"""Retrieves the content of a shared folder."""
logger.debug(f"get_shared_folder_content: Retrieving content for shared folder with ID: {share_id}") logger.debug(f"get_shared_folder_content: Retrieving content for shared folder with ID: {share_id}")
shared_item = await self.get_shared_item(share_id)
shared_item = await self.get_shared_item(share_id, password, accessor_email)
if not shared_item: if not shared_item:
return None return None
user_email = shared_item["user_email"] user_email = shared_item["owner_email"]
item_path = shared_item["item_path"] item_path = shared_item["item_path"]
metadata = await self._load_metadata(user_email) metadata = await self._load_metadata(user_email)
if item_path not in metadata or metadata[item_path]["type"] != "dir": if item_path not in metadata or metadata[item_path]["type"] != "dir":
logger.warning(f"get_shared_folder_content: Shared item is not a directory: {item_path}") logger.warning(f"get_shared_folder_content: Shared item is not a directory: {item_path}")
return None return None

View File

@ -45,7 +45,7 @@ html, body {
/* General typography */ /* General typography */
h1 { h1 {
font-size: 3.5rem; font-size: clamp(1.75rem, 5vw, 3.5rem);
font-weight: 700; font-weight: 700;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
color: var(--text-color); color: var(--text-color);
@ -54,7 +54,7 @@ h1 {
} }
h2 { h2 {
font-size: 2.5rem; font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 700; font-weight: 700;
margin-bottom: 1rem; margin-bottom: 1rem;
color: var(--text-color); color: var(--text-color);
@ -63,7 +63,7 @@ h2 {
} }
h3 { h3 {
font-size: 1.5rem; font-size: clamp(1.125rem, 3vw, 1.5rem);
font-weight: 600; font-weight: 600;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
color: var(--text-color); color: var(--text-color);
@ -73,30 +73,30 @@ h3 {
p { p {
margin-bottom: 1rem; margin-bottom: 1rem;
color: var(--light-text-color); color: var(--light-text-color);
font-size: 1.125rem; font-size: clamp(0.875rem, 2vw, 1.125rem);
line-height: 1.7; line-height: 1.7;
} }
/* Card-like styling for sections */ /* Card-like styling for sections */
.card { .card {
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 8px 25px rgba(0, 0, 0, 0.1);
padding: 3rem; /* Increased padding */ padding: clamp(1rem, 4vw, 3rem);
margin-bottom: 35px; /* Increased margin */ margin-bottom: clamp(20px, 3vw, 35px);
} }
.container { .container {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; /* Allow content to stack vertically */ flex-direction: column;
justify-content: flex-start; /* Align content to the top */ justify-content: flex-start;
align-items: center; align-items: center;
padding: 20px; padding: clamp(10px, 2vw, 20px);
box-sizing: border-box; box-sizing: border-box;
width: 100%; /* Ensure container takes full width */ width: 100%;
max-width: 1200px; /* Max width for overall content */ max-width: 1200px;
margin: 0 auto; /* Center the container */ margin: 0 auto;
} }
.retoors-container { .retoors-container {
@ -468,15 +468,15 @@ main {
@media (max-width: 480px) { @media (max-width: 480px) {
.brand-text { .brand-text {
font-size: 1.25rem; font-size: 1.125rem;
} }
.nav-menu { .nav-menu {
gap: 0.75rem; gap: 0.5rem;
} }
.nav-menu a { .nav-menu a {
font-size: 0.85rem; font-size: 0.8rem;
} }
.nav-actions { .nav-actions {
@ -490,5 +490,67 @@ main {
width: 100%; width: 100%;
max-width: 280px; max-width: 280px;
text-align: center; text-align: center;
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
.form-container {
padding: 1.5rem;
margin: 20px auto;
}
.form-container h2 {
font-size: 1.5rem;
}
.form-group input[type="email"],
.form-group input[type="password"],
.form-group input[type="text"],
.form-group input[type="number"] {
padding: 12px 14px;
}
.btn-primary,
.btn-outline {
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
}
@media (max-width: 360px) {
html, body {
font-size: 14px;
}
.site-nav {
padding: 8px 10px;
}
.brand-text {
font-size: 1rem;
}
.nav-menu a {
font-size: 0.75rem;
}
.card {
padding: 1rem;
margin-bottom: 15px;
}
.container {
padding: 8px;
}
.form-container {
padding: 1rem;
margin: 15px auto;
}
.btn-primary,
.btn-outline {
padding: 0.45rem 0.85rem;
font-size: 0.8rem;
} }
} }

View File

@ -300,7 +300,7 @@
.file-list-table th, .file-list-table th,
.file-list-table td { .file-list-table td {
padding: 10px 12px; padding: 8px 10px;
font-size: 0.85rem; font-size: 0.85rem;
} }
@ -308,3 +308,46 @@
max-width: 150px; max-width: 150px;
} }
} }
@media (max-width: 360px) {
.dashboard-layout {
padding: 8px;
}
.dashboard-sidebar {
padding: 12px;
}
.dashboard-content {
padding: 12px;
}
.dashboard-content-header h2 {
font-size: 1.25rem;
}
.dashboard-actions .btn-primary,
.dashboard-actions .btn-outline {
padding: 0.5rem 0.8rem;
font-size: 0.8rem;
}
.file-list-table {
font-size: 0.75rem;
}
.file-list-table th,
.file-list-table td {
padding: 6px 8px;
font-size: 0.75rem;
}
.file-list-table td {
max-width: 120px;
}
.file-search-bar {
padding: 8px 12px;
font-size: 0.9rem;
}
}

View File

@ -1 +1,106 @@
/* Styles for Form Pages */
.form-page-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: clamp(30px, 5vh, 60px) clamp(15px, 3vw, 20px) clamp(40px, 6vh, 80px);
box-sizing: border-box;
width: 100%;
max-width: 100%;
}
.form-page-container h1 {
font-size: clamp(1.75rem, 4vw, 2.5rem);
color: var(--text-color);
margin-bottom: 10px;
text-align: center;
}
.form-page-container .subtitle {
font-size: clamp(0.95rem, 2vw, 1.1rem);
color: var(--light-text-color);
margin-bottom: clamp(20px, 4vh, 30px);
text-align: center;
max-width: 600px;
}
.form-page-container .form-container {
max-width: 450px;
width: 100%;
padding: clamp(1.5rem, 4vw, 2.5rem);
box-sizing: border-box;
margin-bottom: clamp(20px, 4vh, 30px);
}
.login-link {
display: block;
text-align: center;
margin-top: 25px;
font-size: 0.95rem;
color: var(--light-text-color);
}
.login-link a {
color: var(--primary-color);
font-weight: 500;
}
.login-link a:hover {
color: var(--dutch-red);
text-decoration: underline;
}
.message {
color: #2E7D32;
background-color: #E8F5E9;
border: 1px solid #81C784;
padding: 1rem 1.5rem;
margin-bottom: 1.5rem;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
text-align: center;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.form-page-container {
padding: 30px 15px 40px;
}
.form-page-container h1 {
font-size: 1.75rem;
}
.form-page-container .subtitle {
font-size: 0.95rem;
margin-bottom: 20px;
}
.form-page-container .form-container {
padding: 1.5rem;
}
}
@media (max-width: 360px) {
.form-page-container {
padding: 20px 10px 30px;
}
.form-page-container h1 {
font-size: 1.5rem;
margin-bottom: 8px;
}
.form-page-container .subtitle {
font-size: 0.875rem;
margin-bottom: 15px;
}
.form-page-container .form-container {
padding: 1rem;
}
.login-link {
font-size: 0.85rem;
}
.message {
font-size: 0.85rem;
padding: 0.75rem 1rem;
}
}

View File

@ -1,7 +1,7 @@
/* Hero Section */ /* Hero Section */
.hero-section { .hero-section {
text-align: center; text-align: center;
padding: 80px 20px 60px; padding: clamp(30px, 8vw, 80px) clamp(10px, 3vw, 20px) clamp(20px, 6vw, 60px);
background: linear-gradient(135deg, rgba(33, 70, 139, 0.03) 0%, var(--dutch-white) 100%); background: linear-gradient(135deg, rgba(33, 70, 139, 0.03) 0%, var(--dutch-white) 100%);
margin-bottom: 0; margin-bottom: 0;
border-top: 4px solid transparent; border-top: 4px solid transparent;
@ -55,7 +55,7 @@
/* Features Section */ /* Features Section */
.features-section { .features-section {
padding: 80px 20px; padding: clamp(30px, 8vw, 80px) clamp(10px, 3vw, 20px);
background-color: #FFFFFF; background-color: #FFFFFF;
} }
@ -77,8 +77,8 @@
.features-grid { .features-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
gap: 2.5rem; gap: clamp(1.5rem, 3vw, 2.5rem);
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
} }
@ -109,7 +109,7 @@
/* Pricing Calculator Section */ /* Pricing Calculator Section */
.pricing-calculator { .pricing-calculator {
padding: 80px 20px; padding: clamp(30px, 8vw, 80px) clamp(10px, 3vw, 20px);
background: #FFFFFF; background: #FFFFFF;
text-align: center; text-align: center;
} }
@ -136,13 +136,13 @@
} }
.storage-value { .storage-value {
font-size: 4rem; font-size: clamp(2rem, 8vw, 4rem);
font-weight: 700; font-weight: 700;
color: var(--dutch-blue); color: var(--dutch-blue);
} }
.storage-unit { .storage-unit {
font-size: 2rem; font-size: clamp(1.25rem, 4vw, 2rem);
color: var(--light-text-color); color: var(--light-text-color);
margin-left: 0.5rem; margin-left: 0.5rem;
} }
@ -194,7 +194,7 @@
.price-display { .price-display {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
padding: 2rem; padding: clamp(1rem, 3vw, 2rem);
background: linear-gradient(135deg, rgba(33, 70, 139, 0.05) 0%, rgba(174, 28, 40, 0.05) 100%); background: linear-gradient(135deg, rgba(33, 70, 139, 0.05) 0%, rgba(174, 28, 40, 0.05) 100%);
border-radius: 12px; border-radius: 12px;
border: 2px solid var(--dutch-blue); border: 2px solid var(--dutch-blue);
@ -205,41 +205,42 @@
align-items: baseline; align-items: baseline;
justify-content: center; justify-content: center;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
flex-wrap: wrap;
} }
.currency { .currency {
font-size: 2rem; font-size: clamp(1.25rem, 4vw, 2rem);
color: var(--text-color); color: var(--text-color);
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.price-value { .price-value {
font-size: 4rem; font-size: clamp(2rem, 8vw, 4rem);
font-weight: 700; font-weight: 700;
color: var(--dutch-red); color: var(--dutch-red);
} }
.price-period { .price-period {
font-size: 1.25rem; font-size: clamp(0.875rem, 2.5vw, 1.25rem);
color: var(--light-text-color); color: var(--light-text-color);
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.price-description { .price-description {
font-size: 1.125rem; font-size: clamp(0.875rem, 2.5vw, 1.125rem);
color: var(--dutch-blue); color: var(--dutch-blue);
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
} }
.pricing-calculator .cta-btn { .pricing-calculator .cta-btn {
padding: 1rem 3rem; padding: clamp(0.75rem, 2vw, 1rem) clamp(1.5rem, 5vw, 3rem);
font-size: 1.125rem; font-size: clamp(0.875rem, 2.5vw, 1.125rem);
} }
/* Use Cases Section */ /* Use Cases Section */
.use-cases-section { .use-cases-section {
padding: 80px 20px; padding: clamp(30px, 8vw, 80px) clamp(10px, 3vw, 20px);
background: linear-gradient(135deg, rgba(174, 28, 40, 0.02) 0%, rgba(33, 70, 139, 0.02) 100%); background: linear-gradient(135deg, rgba(174, 28, 40, 0.02) 0%, rgba(33, 70, 139, 0.02) 100%);
text-align: center; text-align: center;
} }
@ -252,8 +253,8 @@
.use-cases-grid { .use-cases-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
gap: 2rem; gap: clamp(1.5rem, 3vw, 2rem);
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
} }
@ -294,7 +295,7 @@
/* CTA Section */ /* CTA Section */
.cta-section { .cta-section {
padding: 100px 20px; padding: clamp(40px, 10vw, 100px) clamp(10px, 3vw, 20px);
background: linear-gradient(135deg, var(--dutch-blue) 0%, var(--dutch-blue-dark) 100%); background: linear-gradient(135deg, var(--dutch-blue) 0%, var(--dutch-blue-dark) 100%);
text-align: center; text-align: center;
color: var(--dutch-white); color: var(--dutch-white);
@ -398,17 +399,135 @@
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.hero-section {
padding: 40px 15px 30px;
}
.hero-section h1 { .hero-section h1 {
font-size: 2rem; font-size: 1.75rem;
} }
.hero-subtitle { .hero-subtitle {
font-size: 1rem; font-size: 0.95rem;
} }
.features-header h2, .features-header h2,
.use-cases-section h2, .use-cases-section h2,
.cta-section h2 {
font-size: 1.5rem;
}
.pricing-calculator {
padding: 40px 15px;
}
.pricing-calculator h2 {
font-size: 1.5rem;
}
.calculator-subtitle {
font-size: 0.9rem;
margin-bottom: 2rem;
}
.storage-display {
margin-bottom: 1.5rem;
}
.slider-container {
margin-bottom: 2rem;
}
.features-section,
.use-cases-section {
padding: 40px 15px;
}
.cta-section {
padding: 40px 15px;
}
.cta-section h2 { .cta-section h2 {
font-size: 1.75rem; font-size: 1.75rem;
} }
.cta-section p {
font-size: 1rem;
}
}
@media (max-width: 360px) {
.hero-section {
padding: 30px 10px 20px;
}
.hero-section h1 {
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
.hero-subtitle {
font-size: 0.875rem;
}
.pricing-calculator {
padding: 30px 10px;
}
.pricing-calculator h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.calculator-subtitle {
font-size: 0.85rem;
margin-bottom: 1.5rem;
}
.storage-display {
margin-bottom: 1rem;
}
.slider-container {
margin-bottom: 1.5rem;
}
.price-display {
margin-bottom: 1.5rem;
padding: 0.75rem;
}
.features-section,
.use-cases-section {
padding: 30px 10px;
}
.features-grid {
gap: 1.5rem;
}
.feature-card {
padding: 1.5rem;
}
.use-cases-grid {
gap: 1.5rem;
}
.use-case-card {
padding: 1.5rem;
}
.cta-section {
padding: 30px 10px;
}
.cta-section h2 {
font-size: 1.5rem;
}
.cta-section p {
font-size: 0.9rem;
margin-bottom: 1.5rem;
}
} }

View File

@ -4,31 +4,33 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
min-height: calc(100vh - 120px); /* Adjust based on header/footer height */ padding: clamp(30px, 5vh, 60px) clamp(15px, 3vw, 20px) clamp(40px, 6vh, 80px);
padding: 20px;
box-sizing: border-box; box-sizing: border-box;
width: 100%;
max-width: 100%;
} }
.login-page-container h1 { .login-page-container h1 {
font-size: 2.5rem; font-size: clamp(1.75rem, 4vw, 2.5rem);
color: var(--text-color); color: var(--text-color);
margin-bottom: 10px; margin-bottom: 10px;
text-align: center; text-align: center;
} }
.login-page-container .subtitle { .login-page-container .subtitle {
font-size: 1.1rem; font-size: clamp(0.95rem, 2vw, 1.1rem);
color: var(--light-text-color); color: var(--light-text-color);
margin-bottom: 30px; margin-bottom: clamp(20px, 4vh, 30px);
text-align: center; text-align: center;
} }
.form-container { .form-container {
max-width: 450px; /* Adjust as needed */ max-width: 450px;
width: 100%; width: 100%;
padding: 2.5rem; padding: clamp(1.5rem, 4vw, 2.5rem);
box-sizing: border-box; box-sizing: border-box;
margin-bottom: clamp(20px, 4vh, 30px);
} }
.forgot-password-link { .forgot-password-link {
@ -66,13 +68,40 @@
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 600px) { @media (max-width: 600px) {
.login-page-container {
padding: 30px 15px 40px;
}
.login-page-container h1 { .login-page-container h1 {
font-size: 2rem; font-size: 1.75rem;
} }
.login-page-container .subtitle { .login-page-container .subtitle {
font-size: 1rem; font-size: 0.95rem;
margin-bottom: 20px;
} }
.form-container { .form-container {
padding: 2rem; padding: 1.5rem;
}
}
@media (max-width: 360px) {
.login-page-container {
padding: 20px 10px 30px;
}
.login-page-container h1 {
font-size: 1.5rem;
margin-bottom: 8px;
}
.login-page-container .subtitle {
font-size: 0.875rem;
margin-bottom: 15px;
}
.form-container {
padding: 1rem;
}
.forgot-password-link {
font-size: 0.85rem;
}
.create-account-link {
font-size: 0.85rem;
} }
} }

View File

@ -374,3 +374,58 @@
width: 100%; width: 100%;
} }
} }
@media (max-width: 360px) {
.overview-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.quota-overview-card {
padding: 10px;
}
.quota-overview-card h2 {
font-size: 1rem;
}
.quota-overview-card .subtitle {
font-size: 0.8rem;
}
.donut-chart-container {
width: 120px;
height: 120px;
}
.donut-chart-container::before {
width: 90px;
height: 90px;
}
.donut-chart-text {
font-size: 0.85rem;
}
.order-form-card {
padding: 10px;
}
.order-form-card h3 {
font-size: 1rem;
}
.user-quotas-section {
padding: 10px;
}
.user-quotas-header h2 {
font-size: 1rem;
}
.user-quota-list {
grid-template-columns: 1fr;
gap: 15px;
}
.user-quota-item {
padding: 15px;
}
.user-quota-item .user-info h4 {
font-size: 1rem;
}
.modal-content {
padding: 15px;
}
.modal-content h3 {
font-size: 1.1rem;
}
}

View File

@ -4,31 +4,33 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
min-height: calc(100vh - 120px); /* Adjust based on header/footer height */ padding: clamp(30px, 5vh, 60px) clamp(15px, 3vw, 20px) clamp(40px, 6vh, 80px);
padding: 20px;
box-sizing: border-box; box-sizing: border-box;
width: 100%;
max-width: 100%;
} }
.register-page-container h1 { .register-page-container h1 {
font-size: 2.5rem; font-size: clamp(1.75rem, 4vw, 2.5rem);
color: var(--text-color); color: var(--text-color);
margin-bottom: 10px; margin-bottom: 10px;
text-align: center; text-align: center;
} }
.register-page-container .subtitle { .register-page-container .subtitle {
font-size: 1.1rem; font-size: clamp(0.95rem, 2vw, 1.1rem);
color: var(--light-text-color); color: var(--light-text-color);
margin-bottom: 30px; margin-bottom: clamp(20px, 4vh, 30px);
text-align: center; text-align: center;
} }
.form-container { .form-container {
max-width: 450px; /* Adjust as needed */ max-width: 450px;
width: 100%; width: 100%;
padding: 2.5rem; padding: clamp(1.5rem, 4vw, 2.5rem);
box-sizing: border-box; box-sizing: border-box;
margin-bottom: clamp(20px, 4vh, 30px);
} }
.terms-checkbox { .terms-checkbox {
@ -77,13 +79,40 @@
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 600px) { @media (max-width: 600px) {
.register-page-container {
padding: 30px 15px 40px;
}
.register-page-container h1 { .register-page-container h1 {
font-size: 2rem; font-size: 1.75rem;
} }
.register-page-container .subtitle { .register-page-container .subtitle {
font-size: 1rem; font-size: 0.95rem;
margin-bottom: 20px;
} }
.form-container { .form-container {
padding: 2rem; padding: 1.5rem;
}
}
@media (max-width: 360px) {
.register-page-container {
padding: 20px 10px 30px;
}
.register-page-container h1 {
font-size: 1.5rem;
margin-bottom: 8px;
}
.register-page-container .subtitle {
font-size: 0.875rem;
margin-bottom: 15px;
}
.form-container {
padding: 1rem;
}
.terms-checkbox {
font-size: 0.85rem;
}
.login-link {
font-size: 0.85rem;
} }
} }

View File

@ -169,7 +169,53 @@
.support-hero h1, .support-categories h2, .contact-options h2 { .support-hero h1, .support-categories h2, .contact-options h2 {
font-size: 1.2rem; font-size: 1.2rem;
} }
.support-hero p {
font-size: 0.85rem;
}
.category-card, .contact-card { .category-card, .contact-card {
padding: 15px; padding: 15px;
} }
.search-support .search-input {
font-size: 0.85rem;
}
}
@media (max-width: 360px) {
.support-hero {
padding: 0;
margin-bottom: 20px;
}
.support-hero h1, .support-categories h2, .contact-options h2 {
font-size: 1.1rem;
}
.support-hero p {
font-size: 0.8rem;
}
.search-support {
max-width: 100%;
margin-bottom: 20px;
}
.search-support .search-input {
padding: 8px 12px;
font-size: 0.8rem;
}
.support-categories {
margin-bottom: 30px;
}
.category-grid, .contact-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.category-card, .contact-card {
padding: 12px;
}
.category-card h3, .contact-card h3 {
font-size: 1rem;
}
.category-card p, .contact-card p {
font-size: 0.85rem;
}
.contact-options {
margin-bottom: 30px;
}
} }

View File

@ -2,18 +2,18 @@
.use-cases-hero { .use-cases-hero {
text-align: center; text-align: center;
padding: 40px 20px; padding: clamp(20px, 5vw, 40px) clamp(10px, 3vw, 20px);
margin-bottom: 60px; margin-bottom: clamp(30px, 6vw, 60px);
} }
.use-cases-hero h1 { .use-cases-hero h1 {
font-size: 3rem; font-size: clamp(1.75rem, 5vw, 3rem);
color: var(--text-color); color: var(--text-color);
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.use-cases-hero p { .use-cases-hero p {
font-size: 1.2rem; font-size: clamp(0.95rem, 2.5vw, 1.2rem);
color: var(--light-text-color); color: var(--light-text-color);
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
@ -21,11 +21,11 @@
.use-case-scenarios { .use-case-scenarios {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
gap: 30px; gap: clamp(20px, 3vw, 30px);
max-width: 1200px; max-width: 1200px;
margin: 0 auto 60px auto; margin: 0 auto clamp(30px, 6vw, 60px) auto;
padding: 0 20px; padding: 0 clamp(10px, 3vw, 20px);
} }
.scenario-card { .scenario-card {
@ -127,9 +127,52 @@
@media (max-width: 480px) { @media (max-width: 480px) {
.use-cases-hero h1 { .use-cases-hero h1 {
font-size: 2rem; font-size: 1.75rem;
}
.use-cases-hero p {
font-size: 0.9rem;
} }
.scenario-card { .scenario-card {
padding: 20px; padding: 20px;
} }
.scenario-card h2 {
font-size: 1.5rem;
}
.use-cases-cta h2 {
font-size: 1.5rem;
}
}
@media (max-width: 360px) {
.use-cases-hero {
padding: 15px 10px;
margin-bottom: 20px;
}
.use-cases-hero h1 {
font-size: 1.5rem;
}
.use-cases-hero p {
font-size: 0.85rem;
}
.use-case-scenarios {
gap: 20px;
padding: 0 10px;
grid-template-columns: 1fr;
min-width: 0;
}
.scenario-card {
padding: 15px;
}
.scenario-card h2 {
font-size: 1.25rem;
}
.scenario-card p {
font-size: 0.9rem;
}
.use-cases-cta {
padding: 20px 10px;
}
.use-cases-cta h2 {
font-size: 1.25rem;
}
} }

View File

@ -115,62 +115,65 @@ document.addEventListener('DOMContentLoaded', () => {
async function shareFile(paths, names) { async function shareFile(paths, names) {
const modal = document.getElementById('share-modal'); const modal = document.getElementById('share-modal');
const linkContainer = document.getElementById('share-link-container');
const loading = document.getElementById('share-loading');
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'); const quickShareResult = document.getElementById('quick-share-result');
const quickShareLinkInput = document.getElementById('quick-share-link-input');
const generateQuickShareBtn = document.getElementById('generate-quick-share-btn');
const advancedShareBtn = document.getElementById('advanced-share-btn');
shareLinkInput.value = ''; quickShareResult.style.display = 'none';
if (shareLinksList) shareLinksList.innerHTML = ''; quickShareLinkInput.value = '';
linkContainer.style.display = 'none';
loading.style.display = 'block';
modal.classList.add('show'); modal.classList.add('show');
const currentPath = paths[0];
const currentName = names[0];
if (paths.length === 1) { if (paths.length === 1) {
shareFileName.textContent = `Sharing: ${names[0]}`; shareFileName.textContent = `Sharing: ${currentName}`;
} else { } else {
shareFileName.textContent = `Sharing ${paths.length} items`; shareFileName.textContent = `Sharing ${paths.length} items`;
} }
try { generateQuickShareBtn.onclick = async function() {
const response = await fetch(`/files/share_multiple`, { generateQuickShareBtn.disabled = true;
method: 'POST', generateQuickShareBtn.textContent = 'Generating...';
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ paths: paths })
});
const data = await response.json();
if (data.share_links && data.share_links.length > 0) { try {
if (data.share_links.length === 1) { const response = await fetch('/api/sharing/create', {
shareLinkInput.value = data.share_links[0]; method: 'POST',
linkContainer.style.display = 'block'; headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
item_path: currentPath,
permission: 'view',
scope: 'public',
expiration_days: 7
})
});
const data = await response.json();
if (data.success) {
quickShareLinkInput.value = data.share_url;
quickShareResult.style.display = 'block';
generateQuickShareBtn.textContent = 'Generate Another Link';
} else { } else {
// Display multiple links alert('Error generating share link: ' + data.error);
if (!shareLinksList) { generateQuickShareBtn.textContent = 'Generate Quick Share Link';
// Create the list if it doesn't exist
const ul = document.createElement('ul');
ul.id = 'share-links-list';
linkContainer.appendChild(ul);
shareLinksList = ul;
}
data.share_links.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `<strong>${item.name}:</strong> <input type="text" value="${item.link}" readonly class="form-input share-link-item-input"> <button class="btn-primary copy-share-link-item-btn" data-link="${item.link}">Copy</button>`;
shareLinksList.appendChild(li);
});
linkContainer.style.display = 'block';
} }
loading.style.display = 'none'; } catch (error) {
} else { console.error('Error generating share link:', error);
loading.textContent = 'Error generating share link(s)'; alert('Error generating share link');
generateQuickShareBtn.textContent = 'Generate Quick Share Link';
} finally {
generateQuickShareBtn.disabled = false;
} }
} catch (error) { };
console.error('Error sharing files:', error);
loading.textContent = 'Error generating share link(s)'; advancedShareBtn.onclick = function() {
} window.location.href = `/sharing/create?item_path=${encodeURIComponent(currentPath)}`;
};
} }
function copyShareLink() { function copyShareLink() {
@ -295,6 +298,20 @@ document.addEventListener('DOMContentLoaded', () => {
copyShareLinkBtn.addEventListener('click', copyShareLink); copyShareLinkBtn.addEventListener('click', copyShareLink);
} }
const copyQuickShareBtn = document.getElementById('copy-quick-share-btn');
if (copyQuickShareBtn) {
copyQuickShareBtn.addEventListener('click', function() {
const input = document.getElementById('quick-share-link-input');
input.select();
navigator.clipboard.writeText(input.value).then(() => {
alert('Share link copied to clipboard');
}).catch(err => {
document.execCommand('copy');
alert('Share link copied to clipboard');
});
});
}
document.getElementById('select-all')?.addEventListener('change', function(e) { document.getElementById('select-all')?.addEventListener('change', function(e) {
const checkboxes = document.querySelectorAll('.file-checkbox'); const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => cb.checked = e.target.checked); checkboxes.forEach(cb => cb.checked = e.target.checked);

View File

@ -5,12 +5,12 @@
<span class="brand-text">Retoor's</span> <span class="brand-text">Retoor's</span>
</a> </a>
<ul class="nav-menu"> <ul class="nav-menu">
{% if request['user'] %} {% if request.get('user') %}
<li><a href="/files">My Files</a></li> <li><a href="/files">My Files</a></li>
{% endif %} {% endif %}
</ul> </ul>
<div class="nav-actions"> <div class="nav-actions">
{% if request['user'] %} {% if request.get('user') %}
<a href="/files" class="nav-link">Dashboard</a> <a href="/files" class="nav-link">Dashboard</a>
<a href="/logout" class="btn-outline">Logout</a> <a href="/logout" class="btn-outline">Logout</a>
{% else %} {% else %}

View File

@ -11,6 +11,7 @@
<div class="sidebar-menu"> <div class="sidebar-menu">
<ul> <ul>
<li><a href="/files" {% if active_page == 'files' %}class="active"{% endif %}><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li> <li><a href="/files" {% if active_page == 'files' %}class="active"{% endif %}><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li>
<li><a href="/sharing/manage" {% if active_page == 'my_shares' %}class="active"{% endif %}><img src="/static/images/icon-professionals.svg" alt="My Shares Icon" class="icon"> My Shares</a></li>
<li><a href="/shared" {% if active_page == 'shared' %}class="active"{% endif %}><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li> <li><a href="/shared" {% if active_page == 'shared' %}class="active"{% endif %}><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li>
<li><a href="/recent" {% if active_page == 'recent' %}class="active"{% endif %}><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li> <li><a href="/recent" {% if active_page == 'recent' %}class="active"{% endif %}><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li>
<li><a href="/favorites" {% if active_page == 'favorites' %}class="active"{% endif %}><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li> <li><a href="/favorites" {% if active_page == 'favorites' %}class="active"{% endif %}><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li>

View File

@ -131,20 +131,46 @@
<div id="share-modal" class="modal"> <div id="share-modal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeModal('share-modal')">&times;</span> <span class="close" onclick="closeModal('share-modal')">&times;</span>
<h3>Share File</h3> <h3>Share Item</h3>
<p id="share-file-name"></p> <p id="share-file-name"></p>
<div id="share-link-container" style="display: none;">
<input type="text" id="share-link-input" readonly class="form-input"> <div class="share-options">
<button class="btn-primary" id="copy-share-link-btn">Copy Link</button> <h4>Quick Share (Public Link)</h4>
<div id="share-links-list" class="share-links-list"></div> <p style="color: #666; font-size: 0.9em; margin-bottom: 1rem;">Generate a simple public link that anyone can access</p>
<div id="quick-share-container">
<button class="btn-primary" id="generate-quick-share-btn">Generate Quick Share Link</button>
<div id="quick-share-result" style="display: none; margin-top: 1rem;">
<input type="text" id="quick-share-link-input" readonly class="form-input">
<button class="btn-outline" id="copy-quick-share-btn">Copy Link</button>
</div>
</div>
<hr style="margin: 2rem 0;">
<h4>Advanced Sharing</h4>
<p style="color: #666; font-size: 0.9em; margin-bottom: 1rem;">Configure permissions, passwords, expiration, and more</p>
<button class="btn-primary" id="advanced-share-btn">Create Advanced Share</button>
</div> </div>
<div id="share-loading">Generating share link...</div>
<div class="modal-actions"> <div class="modal-actions" style="margin-top: 2rem;">
<button type="button" class="btn-outline" onclick="closeModal('share-modal')">Close</button> <button type="button" class="btn-outline" onclick="closeModal('share-modal')">Close</button>
</div> </div>
</div> </div>
</div> </div>
<style>
.share-options {
margin: 1.5rem 0;
}
.share-options h4 {
margin-bottom: 0.5rem;
color: #333;
}
#quick-share-result input {
margin-bottom: 0.5rem;
}
</style>
<div id="delete-modal" class="modal"> <div id="delete-modal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeModal('delete-modal')">&times;</span> <span class="close" onclick="closeModal('delete-modal')">&times;</span>