Update.
This commit is contained in:
parent
2ad7401226
commit
a6a19b8438
@ -10,6 +10,7 @@ import dotenv # Import dotenv
|
|||||||
from .routes import setup_routes
|
from .routes import setup_routes
|
||||||
from .services.user_service import UserService
|
from .services.user_service import UserService
|
||||||
from .services.config_service import ConfigService
|
from .services.config_service import ConfigService
|
||||||
|
from .services.file_service import FileService # Import FileService
|
||||||
from .middlewares import user_middleware, error_middleware
|
from .middlewares import user_middleware, error_middleware
|
||||||
from .helpers.env_manager import ensure_env_file_exists, get_or_create_session_secret_key # Import new function
|
from .helpers.env_manager import ensure_env_file_exists, get_or_create_session_secret_key # Import new function
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ async def setup_services(app: web.Application):
|
|||||||
data_path = base_path.parent / "data"
|
data_path = base_path.parent / "data"
|
||||||
app["user_service"] = UserService(data_path / "users.json")
|
app["user_service"] = UserService(data_path / "users.json")
|
||||||
app["config_service"] = ConfigService(data_path / "config.json")
|
app["config_service"] = ConfigService(data_path / "config.json")
|
||||||
|
app["file_service"] = FileService(data_path / "user_files", data_path / "users.json") # Instantiate FileService
|
||||||
|
|
||||||
# Setup aiojobs scheduler
|
# Setup aiojobs scheduler
|
||||||
app["scheduler"] = aiojobs.Scheduler()
|
app["scheduler"] = aiojobs.Scheduler()
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from .views.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView
|
from .views.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView
|
||||||
from .views.site import SiteView, OrderView, FileBrowserView
|
from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -23,7 +23,17 @@ def setup_routes(app):
|
|||||||
app.router.add_view("/recent", SiteView, name="recent")
|
app.router.add_view("/recent", SiteView, name="recent")
|
||||||
app.router.add_view("/favorites", SiteView, name="favorites")
|
app.router.add_view("/favorites", SiteView, name="favorites")
|
||||||
app.router.add_view("/trash", SiteView, name="trash")
|
app.router.add_view("/trash", SiteView, name="trash")
|
||||||
|
app.router.add_view("/users", SiteView, name="users")
|
||||||
|
app.router.add_view("/users/add", UserManagementView, name="add_user")
|
||||||
|
app.router.add_view("/users/{email}/edit", UserManagementView, name="edit_user")
|
||||||
|
app.router.add_view("/users/{email}/details", UserManagementView, name="user_details")
|
||||||
|
app.router.add_post("/users/{email}/delete", UserManagementView, name="delete_user_page")
|
||||||
app.router.add_view("/files", FileBrowserView, name="file_browser")
|
app.router.add_view("/files", FileBrowserView, name="file_browser")
|
||||||
|
app.router.add_post("/files/new_folder", FileBrowserView, name="new_folder")
|
||||||
|
app.router.add_post("/files/upload", FileBrowserView, name="upload_file")
|
||||||
|
app.router.add_get("/files/download/{file_path:.*}", FileBrowserView, name="download_file")
|
||||||
|
app.router.add_post("/files/share/{file_path:.*}", FileBrowserView, name="share_file")
|
||||||
|
app.router.add_post("/files/delete/{file_path:.*}", FileBrowserView, name="delete_item")
|
||||||
|
|
||||||
# Admin API routes for user and team management
|
# Admin API routes for user and team management
|
||||||
app.router.add_get("/api/users", get_users, name="api_get_users")
|
app.router.add_get("/api/users", get_users, name="api_get_users")
|
||||||
|
|||||||
@ -303,6 +303,63 @@ main {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global Button Styles */
|
||||||
|
.btn-primary,
|
||||||
|
.btn-outline,
|
||||||
|
.btn-small,
|
||||||
|
.btn-danger {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-primary-bg);
|
||||||
|
color: var(--btn-primary-text);
|
||||||
|
border-color: var(--btn-primary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--btn-primary-hover-bg);
|
||||||
|
border-color: var(--btn-primary-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--btn-outline-text);
|
||||||
|
border-color: var(--btn-outline-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background-color: var(--btn-outline-hover-bg);
|
||||||
|
color: var(--btn-outline-text);
|
||||||
|
border-color: var(--btn-outline-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
border-color: #bd2130;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: #D32F2F; /* Red for errors */
|
color: #D32F2F; /* Red for errors */
|
||||||
background-color: #FFEBEE; /* Light red background */
|
background-color: #FFEBEE; /* Light red background */
|
||||||
|
|||||||
@ -1,25 +1,24 @@
|
|||||||
/* General styles for hero sections on content pages */
|
/* General styles for hero sections on content pages */
|
||||||
.hero-intro {
|
.hero-intro {
|
||||||
text-align: center;
|
padding: 20px;
|
||||||
padding: 3rem 1rem;
|
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-headline {
|
.hero-headline {
|
||||||
font-size: 2.5rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-subheadline {
|
.hero-subheadline {
|
||||||
font-size: 1.2rem;
|
font-size: 0.95rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
max-width: 700px;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pricing Plans Section */
|
/* Pricing Plans Section */
|
||||||
@ -34,8 +33,7 @@
|
|||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
padding: 2rem;
|
padding: 20px;
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -53,20 +51,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plan-card h2 {
|
.plan-card h2 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.5rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-card .price {
|
.plan-card .price {
|
||||||
font-size: 3rem;
|
font-size: 2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-card .price span {
|
.plan-card .price span {
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
}
|
}
|
||||||
@ -74,17 +72,16 @@
|
|||||||
.plan-card ul {
|
.plan-card ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.5rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-card ul li {
|
.plan-card ul li {
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.6rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-card ul li i {
|
.plan-card ul li i {
|
||||||
@ -95,9 +92,6 @@
|
|||||||
.plan-card .btn-primary,
|
.plan-card .btn-primary,
|
||||||
.plan-card .btn-outline {
|
.plan-card .btn-outline {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.8rem 1.5rem;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Security Features Section */
|
/* Security Features Section */
|
||||||
@ -112,8 +106,7 @@
|
|||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
padding: 2rem;
|
padding: 20px;
|
||||||
text-align: center;
|
|
||||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,21 +116,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.feature-card i {
|
.feature-card i {
|
||||||
font-size: 3rem; /* Placeholder for icon size */
|
font-size: 2rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.8rem;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card h2 {
|
.feature-card h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card p {
|
.feature-card p {
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FAQ Section */
|
/* FAQ Section */
|
||||||
@ -145,14 +138,14 @@
|
|||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
padding: 2rem;
|
padding: 20px;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-section h2 {
|
.faq-section h2 {
|
||||||
text-align: center;
|
margin-bottom: 20px;
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-item {
|
.faq-item {
|
||||||
@ -168,77 +161,71 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.faq-item h3 {
|
.faq-item h3 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-item p {
|
.faq-item p {
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CTA Section */
|
/* CTA Section */
|
||||||
.cta-section {
|
.cta-section {
|
||||||
text-align: center;
|
padding: 20px;
|
||||||
padding: 3rem 1rem;
|
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-section h2 {
|
.cta-section h2 {
|
||||||
font-size: 2rem;
|
font-size: 1.5rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-section p {
|
.cta-section p {
|
||||||
font-size: 1.1rem;
|
font-size: 0.95rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
max-width: 700px;
|
max-width: 100%;
|
||||||
margin: 0 auto 1.5rem auto;
|
margin: 0 0 1rem 0;
|
||||||
}
|
|
||||||
|
|
||||||
.cta-section .btn-primary {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.hero-headline {
|
.hero-headline {
|
||||||
font-size: 2rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-subheadline {
|
.hero-subheadline {
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricing-plans,
|
.pricing-plans,
|
||||||
.security-features {
|
.security-features {
|
||||||
grid-template-columns: 1fr; /* Stack cards on smaller screens */
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-card,
|
.plan-card,
|
||||||
.feature-card {
|
.feature-card {
|
||||||
padding: 1.5rem;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-card h2,
|
.plan-card h2,
|
||||||
.feature-card h2 {
|
.feature-card h2 {
|
||||||
font-size: 1.6rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-card .price {
|
.plan-card .price {
|
||||||
font-size: 2.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-section h2 {
|
.cta-section h2 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-section p {
|
.cta-section p {
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,50 +1,238 @@
|
|||||||
/* retoors/static/css/components/file_browser.css */
|
.modal {
|
||||||
|
display: none;
|
||||||
.file-browser-section {
|
position: fixed;
|
||||||
padding: 2rem;
|
z-index: 1000;
|
||||||
max-width: 800px;
|
left: 0;
|
||||||
margin: 0 auto;
|
top: 0;
|
||||||
background-color: var(--color-background-light);
|
width: 100%;
|
||||||
border-radius: var(--border-radius);
|
height: 100%;
|
||||||
box-shadow: var(--shadow-elevation-low);
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser-section h1 {
|
.modal-content {
|
||||||
color: var(--color-primary);
|
background-color: var(--card-background);
|
||||||
text-align: center;
|
margin: 10% auto;
|
||||||
margin-bottom: 1.5rem;
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list ul {
|
.modal-content h3 {
|
||||||
list-style: none;
|
margin-top: 0;
|
||||||
padding: 0;
|
margin-bottom: 20px;
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list li {
|
.close {
|
||||||
background-color: var(--color-surface);
|
color: var(--light-text-color);
|
||||||
margin-bottom: 0.5rem;
|
float: right;
|
||||||
padding: 0.8rem 1rem;
|
font-size: 28px;
|
||||||
border-radius: var(--border-radius-small);
|
font-weight: bold;
|
||||||
|
line-height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover,
|
||||||
|
.close:focus {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 188, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small:hover {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
border-color: #bd2130;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-link-input {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-loading {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--light-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-gauge {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-gauge-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--accent-color) 0%, #00d4ff 100%);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border: 1px solid var(--color-border);
|
font-size: 0.85rem;
|
||||||
|
color: var(--light-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list li a {
|
.file-list-table table {
|
||||||
color: var(--color-text);
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th {
|
||||||
|
padding: 12px 15px;
|
||||||
|
text-align: left;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table tr:hover {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table a {
|
||||||
|
color: var(--accent-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
flex-grow: 1;
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list li a:hover {
|
.file-list-table a:hover {
|
||||||
color: var(--color-primary-dark);
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list p {
|
@media (max-width: 768px) {
|
||||||
text-align: center;
|
.modal-content {
|
||||||
color: var(--color-text-secondary);
|
width: 95%;
|
||||||
font-style: italic;
|
margin: 20% auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th,
|
||||||
|
.file-list-table td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-actions button {
|
||||||
|
flex: 1 1 calc(50% - 5px);
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,27 @@
|
|||||||
/* Styles for the Quota Management / Admin Dashboard (order.html) */
|
/* Styles for the Quota Management / Admin Dashboard (order.html) */
|
||||||
|
|
||||||
.order-management-layout {
|
.order-management-layout {
|
||||||
padding: 40px 20px;
|
padding: 0;
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-overview-card {
|
.quota-overview-card {
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 6px 20px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
padding: 30px;
|
padding: 20px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 30px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-overview-card h2 {
|
.quota-overview-card h2 {
|
||||||
font-size: 2.5rem;
|
font-size: 1.8rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-overview-card .subtitle {
|
.quota-overview-card .subtitle {
|
||||||
font-size: 1.1rem;
|
font-size: 0.95rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
margin-bottom: 30px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-grid {
|
.overview-grid {
|
||||||
@ -35,17 +32,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total-storage-chart {
|
.total-storage-chart {
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--background-color); /* Lighter background for this section */
|
background-color: var(--background-color);
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-storage-chart h3 {
|
.total-storage-chart h3 {
|
||||||
font-size: 1.3rem;
|
font-size: 1.1rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donut-chart-container {
|
.donut-chart-container {
|
||||||
@ -96,15 +92,13 @@
|
|||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
padding: 30px;
|
padding: 20px;
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-form-card h3 {
|
.order-form-card h3 {
|
||||||
font-size: 1.3rem;
|
font-size: 1.1rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-form {
|
.order-form {
|
||||||
@ -119,24 +113,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order-form .price-display {
|
.order-form .price-display {
|
||||||
font-size: 1.4rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
text-align: center;
|
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-form .btn-primary {
|
.order-form .btn-primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-quotas-section {
|
.user-quotas-section {
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 6px 20px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
padding: 30px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-quotas-header {
|
.user-quotas-header {
|
||||||
@ -149,16 +140,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-quotas-header h2 {
|
.user-quotas-header h2 {
|
||||||
font-size: 2rem;
|
font-size: 1.8rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-quotas-header .btn-primary {
|
|
||||||
padding: 0.7rem 1.5rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-quota-list {
|
.user-quota-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
@ -176,7 +162,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-quota-item .user-info h4 {
|
.user-quota-item .user-info h4 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
@ -199,16 +185,14 @@
|
|||||||
|
|
||||||
.user-quota-item .quota-actions {
|
.user-quota-item .quota-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap; /* Allow buttons to wrap */
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-quota-item .quota-actions .btn-outline {
|
.user-quota-item .quota-actions .btn-outline {
|
||||||
padding: 0.5rem 1rem;
|
flex-grow: 1;
|
||||||
font-size: 0.85rem;
|
min-width: 100px;
|
||||||
flex-grow: 1; /* Allow buttons to grow and fill space */
|
|
||||||
min-width: 100px; /* Ensure buttons don't get too small */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Small storage gauge for user items */
|
/* Small storage gauge for user items */
|
||||||
@ -259,8 +243,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-size: 1.8rem;
|
font-size: 1.2rem;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content .form-group {
|
.modal-content .form-group {
|
||||||
@ -289,15 +272,12 @@
|
|||||||
|
|
||||||
.modal-content .btn-primary {
|
.modal-content .btn-primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content .error {
|
.modal-content .error {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-button {
|
.close-button {
|
||||||
@ -344,17 +324,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.order-management-layout {
|
|
||||||
padding: 20px 15px;
|
|
||||||
}
|
|
||||||
.quota-overview-card h2 {
|
.quota-overview-card h2 {
|
||||||
font-size: 2rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
.quota-overview-card .subtitle {
|
.quota-overview-card .subtitle {
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.user-quotas-header h2 {
|
.user-quotas-header h2 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
.user-quota-list {
|
.user-quota-list {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@ -367,10 +344,10 @@
|
|||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.quota-overview-card {
|
.quota-overview-card {
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
.quota-overview-card h2 {
|
.quota-overview-card h2 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
.donut-chart-container {
|
.donut-chart-container {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
@ -381,19 +358,19 @@
|
|||||||
height: 110px;
|
height: 110px;
|
||||||
}
|
}
|
||||||
.donut-chart-text {
|
.donut-chart-text {
|
||||||
font-size: 1.2rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
.order-form-card {
|
.order-form-card {
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
.user-quotas-section {
|
.user-quotas-section {
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
.user-quotas-header h2 {
|
.user-quotas-header h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
.user-quota-item .quota-actions .btn-outline {
|
.user-quota-item .quota-actions .btn-outline {
|
||||||
flex-grow: unset; /* Reset flex-grow for smaller screens if needed */
|
flex-grow: unset;
|
||||||
width: 100%; /* Make buttons full width */
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,71 +1,60 @@
|
|||||||
/* Styles for the Support Page (support.html) */
|
/* Styles for the Support Page (support.html) */
|
||||||
|
|
||||||
.support-hero {
|
.support-hero {
|
||||||
text-align: center;
|
padding: 0;
|
||||||
padding: 40px 20px;
|
margin-bottom: 30px;
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-hero h1 {
|
.support-hero h1 {
|
||||||
font-size: 3rem;
|
font-size: 1.8rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-hero p {
|
.support-hero p {
|
||||||
font-size: 1.2rem;
|
font-size: 0.95rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
max-width: 800px;
|
max-width: 100%;
|
||||||
margin: 0 auto 30px auto;
|
margin: 0 0 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-support {
|
.search-support {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-support .search-input {
|
.search-support .search-input {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 12px 20px;
|
padding: 10px 15px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 5px;
|
border-radius: 4px;
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-support .btn-primary {
|
|
||||||
padding: 12px 25px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.support-categories {
|
.support-categories {
|
||||||
text-align: center;
|
margin-bottom: 40px;
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-categories h2 {
|
.support-categories h2 {
|
||||||
font-size: 2.5rem;
|
font-size: 1.5rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 40px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-grid {
|
.category-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
gap: 30px;
|
gap: 20px;
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-card {
|
.category-card {
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 15px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
padding: 30px;
|
padding: 20px;
|
||||||
text-align: center;
|
|
||||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,22 +64,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-card img.icon {
|
.category-card img.icon {
|
||||||
width: 70px;
|
width: 50px;
|
||||||
height: 70px;
|
height: 50px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-card h3 {
|
.category-card h3 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.1rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-card p {
|
.category-card p {
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-card .btn-link {
|
.category-card .btn-link {
|
||||||
@ -104,31 +93,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contact-options {
|
.contact-options {
|
||||||
text-align: center;
|
margin-bottom: 40px;
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-options h2 {
|
.contact-options h2 {
|
||||||
font-size: 2.5rem;
|
font-size: 1.5rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 40px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-grid {
|
.contact-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
gap: 30px;
|
gap: 20px;
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-card {
|
.contact-card {
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 15px var(--shadow-color);
|
box-shadow: 0 4px 12px var(--shadow-color);
|
||||||
padding: 30px;
|
padding: 20px;
|
||||||
text-align: center;
|
|
||||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,27 +122,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contact-card img.icon {
|
.contact-card img.icon {
|
||||||
width: 70px;
|
width: 50px;
|
||||||
height: 70px;
|
height: 50px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-card h3 {
|
.contact-card h3 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.1rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-card p {
|
.contact-card p {
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
}
|
|
||||||
|
|
||||||
.contact-card .btn-primary {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-card .phone-hours {
|
.contact-card .phone-hours {
|
||||||
@ -170,10 +149,10 @@
|
|||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.support-hero h1, .support-categories h2, .contact-options h2 {
|
.support-hero h1, .support-categories h2, .contact-options h2 {
|
||||||
font-size: 2.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
.support-hero p {
|
.support-hero p {
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.search-support {
|
.search-support {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -188,9 +167,9 @@
|
|||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.support-hero h1, .support-categories h2, .contact-options h2 {
|
.support-hero h1, .support-categories h2, .contact-options h2 {
|
||||||
font-size: 2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
.category-card, .contact-card {
|
.category-card, .contact-card {
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<aside class="dashboard-sidebar">
|
<aside class="dashboard-sidebar">
|
||||||
<div class="sidebar-menu">
|
<div class="sidebar-menu">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/dashboard" class="active"><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li>
|
<li><a href="/files" class="active"><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li>
|
||||||
<li><a href="/shared"><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li>
|
<li><a href="/shared"><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li>
|
||||||
<li><a href="/recent"><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li>
|
<li><a href="/recent"><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li>
|
||||||
<li><a href="/favorites"><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li>
|
<li><a href="/favorites"><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li>
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
{% block title %}Favorites - Retoor's Cloud Solutions{% endblock %}
|
{% block title %}Favorites - Retoor's Cloud Solutions{% endblock %}
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
{% block page_title %}Favorites{% endblock %}
|
||||||
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
|
||||||
{% endblock %}
|
{% block dashboard_content %}
|
||||||
{% block content %}
|
<div class="content-section">
|
||||||
<main>
|
<p>Your favorite files and folders will appear here.</p>
|
||||||
<section class="content-section">
|
<p>This feature is coming soon.</p>
|
||||||
<h1>Favorites</h1>
|
</div>
|
||||||
<p>Your favorite files and folders will appear here.</p>
|
|
||||||
<p>This feature is coming soon!</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,24 +1,295 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/dashboard.html" %}
|
||||||
{% block title %}File Browser{% endblock %}
|
|
||||||
{% block head %}
|
{% block title %}My Files - Retoor's Cloud Solutions{% endblock %}
|
||||||
{{ super() }}
|
|
||||||
|
{% block dashboard_head %}
|
||||||
<link rel="stylesheet" href="/static/css/components/file_browser.css">
|
<link rel="stylesheet" href="/static/css/components/file_browser.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
|
||||||
<main>
|
{% block page_title %}My Files{% endblock %}
|
||||||
<section class="file-browser-section">
|
|
||||||
<h1>File Browser</h1>
|
{% block dashboard_actions %}
|
||||||
<div class="file-list">
|
<button class="btn-primary" onclick="showNewFolderModal()">+ New</button>
|
||||||
{% if files %}
|
<button class="btn-outline" onclick="showUploadModal()">Upload</button>
|
||||||
<ul>
|
<button class="btn-outline" onclick="downloadSelected()" id="download-btn" disabled>Download</button>
|
||||||
{% for file in files %}
|
<button class="btn-outline" onclick="shareSelected()" id="share-btn" disabled>Share</button>
|
||||||
<li><a href="/files/{{ file }}" target="_blank">{{ file }}</a></li>
|
<button class="btn-outline" onclick="deleteSelected()" id="delete-btn" disabled>Delete</button>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_content %}
|
||||||
|
|
||||||
|
{% if success_message %}
|
||||||
|
<div class="alert alert-success">
|
||||||
|
{{ success_message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if error_message %}
|
||||||
|
<div class="alert alert-error">
|
||||||
|
{{ error_message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<input type="text" class="file-search-bar" placeholder="Search your files..." id="search-bar">
|
||||||
|
|
||||||
|
<div class="file-list-table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><input type="checkbox" id="select-all"></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Last Modified</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="file-list-body">
|
||||||
|
{% if files %}
|
||||||
|
{% for item in files %}
|
||||||
|
<tr data-path="{{ item.path }}" data-is-dir="{{ item.is_dir }}">
|
||||||
|
<td><input type="checkbox" class="file-checkbox" data-path="{{ item.path }}" data-is-dir="{{ item.is_dir }}"></td>
|
||||||
|
<td>
|
||||||
|
{% if item.is_dir %}
|
||||||
|
<img src="/static/images/icon-families.svg" alt="Folder Icon" class="file-icon">
|
||||||
|
<a href="/files?path={{ item.path }}">{{ item.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
|
||||||
|
{{ item.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>{{ item.last_modified[:10] }}</td>
|
||||||
|
<td>
|
||||||
|
{% if item.is_dir %}
|
||||||
|
--
|
||||||
|
{% else %}
|
||||||
|
{{ (item.size / 1024 / 1024)|round(2) }} MB
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
{% if not item.is_dir %}
|
||||||
|
<button class="btn-small" onclick="downloadFile('{{ item.path }}')">Download</button>
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn-small" onclick="shareFile('{{ item.path }}', '{{ item.name }}')">Share</button>
|
||||||
|
<button class="btn-small btn-danger" onclick="deleteFile('{{ item.path }}', '{{ item.name }}')">Delete</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
{% else %}
|
||||||
{% else %}
|
<tr>
|
||||||
<p>No files found in the project directory.</p>
|
<td colspan="6" style="text-align: center; padding: 40px;">
|
||||||
{% endif %}
|
<p>No files found in this directory.</p>
|
||||||
|
<button class="btn-primary" onclick="showNewFolderModal()" style="margin-top: 10px;">Create your first folder</button>
|
||||||
|
<button class="btn-outline" onclick="showUploadModal()" style="margin-top: 10px;">Upload a file</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="new-folder-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeModal('new-folder-modal')">×</span>
|
||||||
|
<h3>Create New Folder</h3>
|
||||||
|
<form action="/files/new_folder" method="post">
|
||||||
|
<input type="text" name="folder_name" placeholder="Folder name" required class="form-input">
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="submit" class="btn-primary">Create</button>
|
||||||
|
<button type="button" class="btn-outline" onclick="closeModal('new-folder-modal')">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</form>
|
||||||
</main>
|
</div>
|
||||||
{% endblock %}
|
</div>
|
||||||
|
|
||||||
|
<div id="upload-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeModal('upload-modal')">×</span>
|
||||||
|
<h3>Upload File</h3>
|
||||||
|
<form action="/files/upload" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="file" name="file" required class="form-input" id="file-input">
|
||||||
|
<div class="file-info" id="file-info"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="submit" class="btn-primary">Upload</button>
|
||||||
|
<button type="button" class="btn-outline" onclick="closeModal('upload-modal')">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="share-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeModal('share-modal')">×</span>
|
||||||
|
<h3>Share File</h3>
|
||||||
|
<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">
|
||||||
|
<button class="btn-primary" onclick="copyShareLink()">Copy Link</button>
|
||||||
|
</div>
|
||||||
|
<div id="share-loading">Generating share link...</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" class="btn-outline" onclick="closeModal('share-modal')">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="delete-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeModal('delete-modal')">×</span>
|
||||||
|
<h3>Confirm Delete</h3>
|
||||||
|
<p id="delete-message"></p>
|
||||||
|
<form id="delete-form" method="post">
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="submit" class="btn-danger">Delete</button>
|
||||||
|
<button type="button" class="btn-outline" onclick="closeModal('delete-modal')">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showNewFolderModal() {
|
||||||
|
document.getElementById('new-folder-modal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUploadModal() {
|
||||||
|
document.getElementById('upload-modal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(modalId) {
|
||||||
|
document.getElementById(modalId).style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target.classList.contains('modal')) {
|
||||||
|
event.target.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('file-input').addEventListener('change', function(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const size = (file.size / 1024 / 1024).toFixed(2);
|
||||||
|
document.getElementById('file-info').innerHTML = `Selected: ${file.name} (${size} MB)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function downloadFile(path) {
|
||||||
|
window.location.href = `/files/download/${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shareFile(path, name) {
|
||||||
|
const modal = document.getElementById('share-modal');
|
||||||
|
const linkContainer = document.getElementById('share-link-container');
|
||||||
|
const loading = document.getElementById('share-loading');
|
||||||
|
|
||||||
|
document.getElementById('share-file-name').textContent = `Sharing: ${name}`;
|
||||||
|
linkContainer.style.display = 'none';
|
||||||
|
loading.style.display = 'block';
|
||||||
|
modal.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/files/share/${path}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.share_link) {
|
||||||
|
document.getElementById('share-link-input').value = data.share_link;
|
||||||
|
linkContainer.style.display = 'block';
|
||||||
|
loading.style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
loading.textContent = 'Error generating share link';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyShareLink() {
|
||||||
|
const input = document.getElementById('share-link-input');
|
||||||
|
input.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert('Share link copied to clipboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile(path, name) {
|
||||||
|
document.getElementById('delete-message').textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
|
||||||
|
document.getElementById('delete-form').action = `/files/delete/${path}`;
|
||||||
|
document.getElementById('delete-modal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('select-all').addEventListener('change', function(e) {
|
||||||
|
const checkboxes = document.querySelectorAll('.file-checkbox');
|
||||||
|
checkboxes.forEach(cb => cb.checked = e.target.checked);
|
||||||
|
updateActionButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.file-checkbox').forEach(cb => {
|
||||||
|
cb.addEventListener('change', updateActionButtons);
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateActionButtons() {
|
||||||
|
const checked = document.querySelectorAll('.file-checkbox:checked');
|
||||||
|
const downloadBtn = document.getElementById('download-btn');
|
||||||
|
const shareBtn = document.getElementById('share-btn');
|
||||||
|
const deleteBtn = document.getElementById('delete-btn');
|
||||||
|
|
||||||
|
const hasSelection = checked.length > 0;
|
||||||
|
const hasFiles = Array.from(checked).some(cb => cb.dataset.isDir === 'False');
|
||||||
|
|
||||||
|
downloadBtn.disabled = !hasFiles;
|
||||||
|
shareBtn.disabled = !hasSelection;
|
||||||
|
deleteBtn.disabled = !hasSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSelected() {
|
||||||
|
const checked = document.querySelectorAll('.file-checkbox:checked');
|
||||||
|
if (checked.length === 1 && checked[0].dataset.isDir === 'False') {
|
||||||
|
downloadFile(checked[0].dataset.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareSelected() {
|
||||||
|
const checked = document.querySelectorAll('.file-checkbox:checked');
|
||||||
|
if (checked.length === 1) {
|
||||||
|
const path = checked[0].dataset.path;
|
||||||
|
const name = checked[0].closest('tr').querySelector('td:nth-child(2)').textContent.trim();
|
||||||
|
shareFile(path, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
const checked = document.querySelectorAll('.file-checkbox:checked');
|
||||||
|
if (checked.length > 0) {
|
||||||
|
const paths = Array.from(checked).map(cb => cb.dataset.path);
|
||||||
|
const names = Array.from(checked).map(cb =>
|
||||||
|
cb.closest('tr').querySelector('td:nth-child(2)').textContent.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (checked.length === 1) {
|
||||||
|
deleteFile(paths[0], names[0]);
|
||||||
|
} else {
|
||||||
|
document.getElementById('delete-message').textContent =
|
||||||
|
`Are you sure you want to delete ${checked.length} items? This action cannot be undone.`;
|
||||||
|
document.getElementById('delete-modal').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('search-bar').addEventListener('input', function(e) {
|
||||||
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
|
const rows = document.querySelectorAll('#file-list-body tr');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const name = row.querySelector('td:nth-child(2)')?.textContent.toLowerCase();
|
||||||
|
if (name && name.includes(searchTerm)) {
|
||||||
|
row.style.display = '';
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
{% block title %}Quota Management - Retoor's Cloud Solutions{% endblock %}
|
{% block title %}Quota Management - Retoor's Cloud Solutions{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block dashboard_head %}
|
||||||
<link rel="stylesheet" href="/static/css/components/order.css">
|
<link rel="stylesheet" href="/static/css/components/order.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block page_title %}Manage Quota{% endblock %}
|
||||||
<main class="order-management-layout">
|
|
||||||
|
{% block dashboard_content %}
|
||||||
|
<div class="order-management-layout">
|
||||||
<section class="quota-overview-card">
|
<section class="quota-overview-card">
|
||||||
<h2>Optimize Your Team's Storage</h2>
|
<h2>Optimize Your Team's Storage</h2>
|
||||||
<p class="subtitle">Flexible cloud monitoring for teams with ease.</p>
|
<p class="subtitle">Flexible cloud monitoring for teams with ease.</p>
|
||||||
@ -46,75 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="user-quotas-section">
|
</div>
|
||||||
<div class="user-quotas-header">
|
|
||||||
<h2>User & Department Quotas</h2>
|
|
||||||
<button class="btn-primary" id="add-new-user-btn">+ Add New User</button>
|
|
||||||
</div>
|
|
||||||
<div class="user-quota-list" id="user-quota-list">
|
|
||||||
{# User quota items will be dynamically loaded here by JavaScript #}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{# Add New User Modal #}
|
<script src="/static/js/components/order_form.js"></script>
|
||||||
<div id="add-user-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-button">×</span>
|
|
||||||
<h3>Add New User</h3>
|
|
||||||
<form id="add-user-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new-user-full-name">Full Name</label>
|
|
||||||
<input type="text" id="new-user-full-name" name="full_name" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new-user-email">Email</label>
|
|
||||||
<input type="email" id="new-user-email" name="email" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new-user-password">Password</label>
|
|
||||||
<input type="password" id="new-user-password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new-user-confirm-password">Confirm Password</label>
|
|
||||||
<input type="password" id="new-user-confirm-password" name="confirm_password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn-primary">Add User</button>
|
|
||||||
<p id="add-user-message" class="error"></p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Edit Quota Modal #}
|
|
||||||
<div id="edit-quota-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-button">×</span>
|
|
||||||
<h3>Edit User Quota</h3>
|
|
||||||
<form id="edit-quota-form">
|
|
||||||
<input type="hidden" id="edit-quota-user-email">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="edit-quota-amount">Storage Quota (GB)</label>
|
|
||||||
<input type="number" id="edit-quota-amount" name="new_quota_gb" min="1" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn-primary">Update Quota</button>
|
|
||||||
<p id="edit-quota-message" class="error"></p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# View Details Modal #}
|
|
||||||
<div id="view-details-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-button">×</span>
|
|
||||||
<h3>User Details</h3>
|
|
||||||
<div id="user-details-content">
|
|
||||||
{# User details will be loaded here #}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="/static/js/components/order.js"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,15 +1,12 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
{% block title %}Recent Files - Retoor's Cloud Solutions{% endblock %}
|
{% block title %}Recent Files - Retoor's Cloud Solutions{% endblock %}
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
{% block page_title %}Recent Files{% endblock %}
|
||||||
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
|
||||||
{% endblock %}
|
{% block dashboard_content %}
|
||||||
{% block content %}
|
<div class="content-section">
|
||||||
<main>
|
<p>Your recently accessed files will appear here.</p>
|
||||||
<section class="content-section">
|
<p>This feature is coming soon.</p>
|
||||||
<h1>Recent Files</h1>
|
</div>
|
||||||
<p>Your recently accessed files will appear here.</p>
|
|
||||||
<p>This feature is coming soon!</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,15 +1,12 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
{% block title %}Shared with me - Retoor's Cloud Solutions{% endblock %}
|
{% block title %}Shared with me - Retoor's Cloud Solutions{% endblock %}
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
{% block page_title %}Shared with me{% endblock %}
|
||||||
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
|
||||||
{% endblock %}
|
{% block dashboard_content %}
|
||||||
{% block content %}
|
<div class="content-section">
|
||||||
<main>
|
<p>Files and folders that have been shared with you will appear here.</p>
|
||||||
<section class="content-section">
|
<p>This feature is coming soon.</p>
|
||||||
<h1>Shared with me</h1>
|
</div>
|
||||||
<p>Files and folders that have been shared with you will appear here.</p>
|
|
||||||
<p>This feature is coming soon!</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,13 +1,14 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
{% block title %}Support - Retoor's Cloud Solutions{% endblock %}
|
{% block title %}Support - Retoor's Cloud Solutions{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block dashboard_head %}
|
||||||
<link rel="stylesheet" href="/static/css/components/support.css">
|
<link rel="stylesheet" href="/static/css/components/support.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block page_title %}Help & Support{% endblock %}
|
||||||
<main>
|
|
||||||
|
{% block dashboard_content %}
|
||||||
<section class="support-hero">
|
<section class="support-hero">
|
||||||
<h1>We're Here to Help You Succeed</h1>
|
<h1>We're Here to Help You Succeed</h1>
|
||||||
<p>Find answers to your questions, troubleshoot issues, or contact our support team for personalized assistance.</p>
|
<p>Find answers to your questions, troubleshoot issues, or contact our support team for personalized assistance.</p>
|
||||||
@ -70,5 +71,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,15 +1,12 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
{% block title %}Trash - Retoor's Cloud Solutions{% endblock %}
|
{% block title %}Trash - Retoor's Cloud Solutions{% endblock %}
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
{% block page_title %}Trash{% endblock %}
|
||||||
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
|
||||||
{% endblock %}
|
{% block dashboard_content %}
|
||||||
{% block content %}
|
<div class="content-section">
|
||||||
<main>
|
<p>Files and folders you have deleted will appear here.</p>
|
||||||
<section class="content-section">
|
<p>This feature is coming soon.</p>
|
||||||
<h1>Trash</h1>
|
</div>
|
||||||
<p>Files and folders you have deleted will appear here.</p>
|
|
||||||
<p>This feature is coming soon!</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -2,11 +2,13 @@ from aiohttp import web
|
|||||||
import aiohttp_jinja2
|
import aiohttp_jinja2
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from aiohttp.web_response import json_response
|
||||||
|
|
||||||
from ..helpers.auth import login_required
|
from ..helpers.auth import login_required
|
||||||
from .auth import CustomPydanticView
|
from .auth import CustomPydanticView
|
||||||
|
|
||||||
PROJECT_DIR = Path(__file__).parent.parent.parent / "project"
|
# PROJECT_DIR is no longer directly used for user files, as FileService manages them
|
||||||
|
# PROJECT_DIR = Path(__file__).parent.parent.parent / "project"
|
||||||
|
|
||||||
|
|
||||||
class SiteView(web.View):
|
class SiteView(web.View):
|
||||||
@ -35,17 +37,15 @@ class SiteView(web.View):
|
|||||||
return await self.favorites()
|
return await self.favorites()
|
||||||
elif self.request.path == "/trash":
|
elif self.request.path == "/trash":
|
||||||
return await self.trash()
|
return await self.trash()
|
||||||
|
elif self.request.path == "/users":
|
||||||
|
return await self.users()
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"pages/index.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
"pages/index.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
)
|
)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
async def dashboard(self):
|
async def dashboard(self):
|
||||||
return aiohttp_jinja2.render_template(
|
return web.HTTPFound(self.request.app.router["file_browser"].url_for())
|
||||||
"pages/dashboard.html",
|
|
||||||
self.request,
|
|
||||||
{"user": self.request["user"], "request": self.request, "errors": {}},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def solutions(self):
|
async def solutions(self):
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
@ -62,9 +62,10 @@ class SiteView(web.View):
|
|||||||
"pages/security.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
"pages/security.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
async def support(self):
|
async def support(self):
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"pages/support.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
"pages/support.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "support"}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def use_cases(self):
|
async def use_cases(self):
|
||||||
@ -82,42 +83,201 @@ class SiteView(web.View):
|
|||||||
"pages/privacy.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
"pages/privacy.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
async def shared(self):
|
async def shared(self):
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"pages/shared.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
"pages/shared.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "shared"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
async def recent(self):
|
async def recent(self):
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
"pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "recent"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
async def favorites(self):
|
async def favorites(self):
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"pages/favorites.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
"pages/favorites.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "favorites"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
async def trash(self):
|
async def trash(self):
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"pages/trash.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
"pages/trash.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "trash"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
async def users(self):
|
||||||
|
success_message = self.request.query.get("success")
|
||||||
|
error_message = self.request.query.get("error")
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/users.html", self.request, {
|
||||||
|
"request": self.request,
|
||||||
|
"errors": {},
|
||||||
|
"user": self.request["user"],
|
||||||
|
"active_page": "users",
|
||||||
|
"success_message": success_message,
|
||||||
|
"error_message": error_message
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FileBrowserView(web.View):
|
class FileBrowserView(web.View):
|
||||||
@login_required
|
@login_required
|
||||||
async def get(self):
|
async def get(self):
|
||||||
files = []
|
if self.request.match_info.get("file_path") is not None:
|
||||||
if PROJECT_DIR.is_dir():
|
return await self.get_download_file()
|
||||||
for item in os.listdir(PROJECT_DIR):
|
|
||||||
item_path = PROJECT_DIR / item
|
user_email = self.request["user"]["email"]
|
||||||
if item_path.is_file():
|
file_service = self.request.app["file_service"]
|
||||||
files.append(item)
|
|
||||||
|
path = self.request.query.get("path", "")
|
||||||
|
files = await file_service.list_files(user_email, path)
|
||||||
|
|
||||||
|
success_message = self.request.query.get("success")
|
||||||
|
error_message = self.request.query.get("error")
|
||||||
|
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"pages/file_browser.html",
|
"pages/file_browser.html",
|
||||||
self.request,
|
self.request,
|
||||||
{"request": self.request, "files": files, "user": self.request.get("user")},
|
{
|
||||||
|
"request": self.request,
|
||||||
|
"files": files,
|
||||||
|
"user": self.request["user"],
|
||||||
|
"current_path": path,
|
||||||
|
"success_message": success_message,
|
||||||
|
"error_message": error_message,
|
||||||
|
"active_page": "files",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
async def post(self):
|
||||||
|
user_email = self.request["user"]["email"]
|
||||||
|
file_service = self.request.app["file_service"]
|
||||||
|
route_name = self.request.match_info.route.name
|
||||||
|
|
||||||
|
if route_name == "new_folder":
|
||||||
|
data = await self.request.post()
|
||||||
|
folder_name = data.get("folder_name")
|
||||||
|
if not folder_name:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
error="Folder name is required"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
success = await file_service.create_folder(user_email, folder_name)
|
||||||
|
if success:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
success=f"Folder '{folder_name}' created successfully"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
error=f"Folder '{folder_name}' already exists or could not be created"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif route_name == "upload_file":
|
||||||
|
try:
|
||||||
|
reader = await self.request.multipart()
|
||||||
|
field = await reader.next()
|
||||||
|
if not field or field.name != "file":
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
error="No file selected for upload"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
filename = field.filename
|
||||||
|
if not filename:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
error="Filename is required"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
content = await field.read()
|
||||||
|
success = await file_service.upload_file(user_email, filename, content)
|
||||||
|
if success:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
success=f"File '{filename}' uploaded successfully"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
error=f"Failed to upload file '{filename}'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
error=f"Upload error: {str(e)}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif route_name == "share_file":
|
||||||
|
file_path = self.request.match_info.get("file_path")
|
||||||
|
if not file_path:
|
||||||
|
return json_response({"error": "File path is required for sharing"}, status=400)
|
||||||
|
|
||||||
|
share_id = await file_service.generate_share_link(user_email, file_path)
|
||||||
|
if share_id:
|
||||||
|
share_link = f"{self.request.scheme}://{self.request.host}/shared_file/{share_id}"
|
||||||
|
return json_response({"share_link": share_link})
|
||||||
|
else:
|
||||||
|
return json_response({"error": "Failed to generate share link"}, status=500)
|
||||||
|
|
||||||
|
elif route_name == "delete_item":
|
||||||
|
item_path = self.request.match_info.get("file_path")
|
||||||
|
if not item_path:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
error="Item path is required for deletion"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
success = await file_service.delete_item(user_email, item_path)
|
||||||
|
if success:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
success=f"Item deleted successfully"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["file_browser"].url_for().with_query(
|
||||||
|
error="Failed to delete item - it may not exist"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return web.HTTPBadRequest(text="Unknown file action")
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
async def get_download_file(self):
|
||||||
|
user_email = self.request["user"]["email"]
|
||||||
|
file_service = self.request.app["file_service"]
|
||||||
|
file_path = self.request.match_info.get("file_path")
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return web.HTTPBadRequest(text="File path is required for download")
|
||||||
|
|
||||||
|
result = await file_service.download_file(user_email, file_path)
|
||||||
|
if result:
|
||||||
|
content, filename = result
|
||||||
|
response = web.Response(body=content)
|
||||||
|
response.headers["Content-Disposition"] = f"attachment; filename={filename}"
|
||||||
|
response.headers["Content-Type"] = "application/octet-stream"
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
return web.HTTPNotFound(text="File not found")
|
||||||
|
|
||||||
|
|
||||||
class OrderView(CustomPydanticView):
|
class OrderView(CustomPydanticView):
|
||||||
template_name = "pages/order.html"
|
template_name = "pages/order.html"
|
||||||
@ -130,22 +290,236 @@ class OrderView(CustomPydanticView):
|
|||||||
self.template_name, self.request, {
|
self.template_name, self.request, {
|
||||||
"request": self.request,
|
"request": self.request,
|
||||||
"errors": {},
|
"errors": {},
|
||||||
"user": self.request.get("user"),
|
"user": self.request["user"],
|
||||||
"price_per_gb": price_per_gb
|
"price_per_gb": price_per_gb,
|
||||||
|
"active_page": "order"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
async def post(self):
|
async def post(self):
|
||||||
# The quota update for the main user is now handled via AJAX in order.js
|
|
||||||
# This POST method will simply re-render the page.
|
|
||||||
config_service = self.request.app["config_service"]
|
config_service = self.request.app["config_service"]
|
||||||
price_per_gb = config_service.get_price_per_gb()
|
price_per_gb = config_service.get_price_per_gb()
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
self.template_name, self.request, {
|
self.template_name, self.request, {
|
||||||
"request": self.request,
|
"request": self.request,
|
||||||
"errors": {},
|
"errors": {},
|
||||||
"user": self.request.get("user"),
|
"user": self.request["user"],
|
||||||
"price_per_gb": price_per_gb
|
"price_per_gb": price_per_gb,
|
||||||
|
"active_page": "order"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserManagementView(web.View):
|
||||||
|
@login_required
|
||||||
|
async def get(self):
|
||||||
|
route_name = self.request.match_info.route.name
|
||||||
|
|
||||||
|
if route_name == "add_user":
|
||||||
|
return await self.add_user_page()
|
||||||
|
elif route_name == "edit_user":
|
||||||
|
return await self.edit_user_page()
|
||||||
|
elif route_name == "user_details":
|
||||||
|
return await self.user_details_page()
|
||||||
|
|
||||||
|
return web.HTTPNotFound()
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
async def post(self):
|
||||||
|
route_name = self.request.match_info.route.name
|
||||||
|
|
||||||
|
if route_name == "add_user":
|
||||||
|
return await self.add_user_submit()
|
||||||
|
elif route_name == "edit_user":
|
||||||
|
return await self.edit_user_submit()
|
||||||
|
elif route_name == "delete_user_page":
|
||||||
|
return await self.delete_user_submit()
|
||||||
|
|
||||||
|
return web.HTTPNotFound()
|
||||||
|
|
||||||
|
async def add_user_page(self):
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/add_user.html",
|
||||||
|
self.request,
|
||||||
|
{
|
||||||
|
"request": self.request,
|
||||||
|
"user": self.request["user"],
|
||||||
|
"errors": {},
|
||||||
|
"form_data": {},
|
||||||
|
"active_page": "users"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def add_user_submit(self):
|
||||||
|
data = await self.request.post()
|
||||||
|
full_name = data.get("full_name", "").strip()
|
||||||
|
email = data.get("email", "").strip()
|
||||||
|
password = data.get("password", "")
|
||||||
|
confirm_password = data.get("confirm_password", "")
|
||||||
|
storage_quota_gb = data.get("storage_quota_gb", "10")
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if not full_name:
|
||||||
|
errors["full_name"] = "Full name is required"
|
||||||
|
if not email:
|
||||||
|
errors["email"] = "Email is required"
|
||||||
|
if not password:
|
||||||
|
errors["password"] = "Password is required"
|
||||||
|
if password != confirm_password:
|
||||||
|
errors["confirm_password"] = "Passwords do not match"
|
||||||
|
|
||||||
|
try:
|
||||||
|
storage_quota_gb = int(storage_quota_gb)
|
||||||
|
if storage_quota_gb < 1:
|
||||||
|
errors["storage_quota_gb"] = "Storage quota must be at least 1 GB"
|
||||||
|
except ValueError:
|
||||||
|
errors["storage_quota_gb"] = "Invalid storage quota value"
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/add_user.html",
|
||||||
|
self.request,
|
||||||
|
{
|
||||||
|
"request": self.request,
|
||||||
|
"user": self.request["user"],
|
||||||
|
"errors": errors,
|
||||||
|
"form_data": data,
|
||||||
|
"active_page": "users"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
user_service = self.request.app["user_service"]
|
||||||
|
parent_email = self.request["user"]["email"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_user = user_service.create_user(
|
||||||
|
full_name=full_name,
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
parent_email=parent_email
|
||||||
|
)
|
||||||
|
|
||||||
|
user_service.update_user_quota(email, float(storage_quota_gb))
|
||||||
|
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["users"].url_for().with_query(
|
||||||
|
success=f"User {email} added successfully"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
errors["email"] = str(e)
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/add_user.html",
|
||||||
|
self.request,
|
||||||
|
{
|
||||||
|
"request": self.request,
|
||||||
|
"user": self.request["user"],
|
||||||
|
"errors": errors,
|
||||||
|
"form_data": data,
|
||||||
|
"active_page": "users"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def edit_user_page(self):
|
||||||
|
email = self.request.match_info.get("email")
|
||||||
|
|
||||||
|
user_service = self.request.app["user_service"]
|
||||||
|
user_data = user_service.get_user_by_email(email)
|
||||||
|
|
||||||
|
if not user_data:
|
||||||
|
return web.HTTPNotFound(text="User not found")
|
||||||
|
|
||||||
|
success_message = self.request.query.get("success")
|
||||||
|
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/edit_user.html",
|
||||||
|
self.request,
|
||||||
|
{
|
||||||
|
"request": self.request,
|
||||||
|
"user": self.request["user"],
|
||||||
|
"user_data": user_data,
|
||||||
|
"errors": {},
|
||||||
|
"success_message": success_message,
|
||||||
|
"active_page": "users"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def edit_user_submit(self):
|
||||||
|
email = self.request.match_info.get("email")
|
||||||
|
data = await self.request.post()
|
||||||
|
storage_quota_gb = data.get("storage_quota_gb", "")
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
storage_quota_gb = float(storage_quota_gb)
|
||||||
|
if storage_quota_gb < 1:
|
||||||
|
errors["storage_quota_gb"] = "Storage quota must be at least 1 GB"
|
||||||
|
except ValueError:
|
||||||
|
errors["storage_quota_gb"] = "Invalid storage quota value"
|
||||||
|
|
||||||
|
user_service = self.request.app["user_service"]
|
||||||
|
user_data = user_service.get_user_by_email(email)
|
||||||
|
|
||||||
|
if not user_data:
|
||||||
|
return web.HTTPNotFound(text="User not found")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/edit_user.html",
|
||||||
|
self.request,
|
||||||
|
{
|
||||||
|
"request": self.request,
|
||||||
|
"user": self.request["user"],
|
||||||
|
"user_data": user_data,
|
||||||
|
"errors": errors,
|
||||||
|
"active_page": "users"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
user_service.update_user_quota(email, storage_quota_gb)
|
||||||
|
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["edit_user"].url_for(email=email).with_query(
|
||||||
|
success="User quota updated successfully"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def user_details_page(self):
|
||||||
|
email = self.request.match_info.get("email")
|
||||||
|
|
||||||
|
user_service = self.request.app["user_service"]
|
||||||
|
user_data = user_service.get_user_by_email(email)
|
||||||
|
|
||||||
|
if not user_data:
|
||||||
|
return web.HTTPNotFound(text="User not found")
|
||||||
|
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/user_details.html",
|
||||||
|
self.request,
|
||||||
|
{
|
||||||
|
"request": self.request,
|
||||||
|
"user": self.request["user"],
|
||||||
|
"user_data": user_data,
|
||||||
|
"active_page": "users"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete_user_submit(self):
|
||||||
|
email = self.request.match_info.get("email")
|
||||||
|
|
||||||
|
user_service = self.request.app["user_service"]
|
||||||
|
user_data = user_service.get_user_by_email(email)
|
||||||
|
|
||||||
|
if not user_data:
|
||||||
|
return web.HTTPNotFound(text="User not found")
|
||||||
|
|
||||||
|
user_service.delete_user(email)
|
||||||
|
|
||||||
|
return web.HTTPFound(
|
||||||
|
self.request.app.router["users"].url_for().with_query(
|
||||||
|
success=f"User {email} deleted successfully"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@ -190,10 +190,8 @@ async def test_file_browser_get_authorized(client):
|
|||||||
resp = await client.get("/files")
|
resp = await client.get("/files")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
text = await resp.text()
|
text = await resp.text()
|
||||||
assert "File Browser" in text
|
assert "My Files" in text
|
||||||
# Check for some expected files from the project directory
|
assert "No files found in this directory." in text
|
||||||
assert "example.jpg" in text
|
|
||||||
assert "rexample7.jpg" in text
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user