Update.
This commit is contained in:
parent
155992f196
commit
c281f1e9ea
@ -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")
|
||||||
|
|||||||
@ -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]
|
|
||||||
expiry_time = datetime.datetime.fromisoformat(shared_item["expires_at"])
|
|
||||||
if expiry_time > datetime.datetime.now(datetime.timezone.utc):
|
|
||||||
logger.info(f"get_shared_item: Found valid shared item for ID: {share_id}")
|
|
||||||
return shared_item
|
|
||||||
else:
|
|
||||||
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}")
|
logger.warning(f"get_shared_item: No valid shared item found for ID: {share_id}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_shared_file_content(self, share_id: str, requested_file_path: str | None = None) -> tuple[bytes, str] | None:
|
if not await self.sharing_service.verify_share_access(share_id, password, accessor_email):
|
||||||
"""Retrieves the content of a shared file."""
|
logger.warning(f"get_shared_item: Access denied for share {share_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
await self.sharing_service.record_share_access(share_id, accessor_email)
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateQuickShareBtn.onclick = async function() {
|
||||||
|
generateQuickShareBtn.disabled = true;
|
||||||
|
generateQuickShareBtn.textContent = 'Generating...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/files/share_multiple`, {
|
const response = await fetch('/api/sharing/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ paths: paths })
|
body: JSON.stringify({
|
||||||
|
item_path: currentPath,
|
||||||
|
permission: 'view',
|
||||||
|
scope: 'public',
|
||||||
|
expiration_days: 7
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.share_links && data.share_links.length > 0) {
|
if (data.success) {
|
||||||
if (data.share_links.length === 1) {
|
quickShareLinkInput.value = data.share_url;
|
||||||
shareLinkInput.value = data.share_links[0];
|
quickShareResult.style.display = 'block';
|
||||||
linkContainer.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';
|
|
||||||
} else {
|
|
||||||
loading.textContent = 'Error generating share link(s)';
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sharing files:', error);
|
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;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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')">×</span>
|
<span class="close" onclick="closeModal('share-modal')">×</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>
|
||||||
<div id="share-loading">Generating share link...</div>
|
</div>
|
||||||
<div class="modal-actions">
|
|
||||||
|
<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 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')">×</span>
|
<span class="close" onclick="closeModal('delete-modal')">×</span>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user