Minor UI Edits
This commit is contained in:
parent
315746852b
commit
0f671fa1b1
|
@ -40,3 +40,4 @@
|
||||||
.read-the-docs {
|
.read-the-docs {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,14 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const [openDropdown, setOpenDropdown] = useState<number | null>(null);
|
const [openDropdown, setOpenDropdown] = useState<number | null>(null);
|
||||||
const [token, setToken] = useState<string | null>(null);
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
const [shareDuration, setShareDuration] = useState(7); // durata di default 7 giorni
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const dropdownRef = useRef<HTMLTableDataCellElement | null>(null);
|
const dropdownRef = useRef<HTMLTableDataCellElement | null>(null);
|
||||||
|
|
||||||
// Recupera il token solo una volta al mount o quando cambia
|
// Recupera il token solo una volta al mount o quando cambia
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedToken = localStorage.getItem('token');
|
const storedToken = localStorage.getItem('litecloud_token'); // <-- usa la chiave corretta
|
||||||
setToken(storedToken);
|
setToken(storedToken);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await apiCall('http://192.168.1.120:8082/api/files');
|
const response = await apiCall('/api/files');
|
||||||
const filesData = await response.json();
|
const filesData = await response.json();
|
||||||
setFiles(filesData);
|
setFiles(filesData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -90,8 +91,8 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('filename', file.name);
|
formData.append('filename', file.name);
|
||||||
|
|
||||||
const token = getAuthToken();
|
if (!token) throw new Error('No auth token found');
|
||||||
const response = await fetch('http://192.168.1.120:8082/api/upload', {
|
const response = await fetch('/api/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
|
@ -106,7 +107,7 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
|
||||||
throw new Error('Upload failed');
|
throw new Error('Upload failed');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to upload file');
|
setError('Failed to upload file - ' + (err instanceof Error ? err.message : 'Unknown error'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
|
@ -115,24 +116,26 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileAction = async (action: 'share' | 'delete', fileId: number) => {
|
const handleFileAction = async (
|
||||||
|
action: 'share' | 'delete',
|
||||||
|
fileId: number,
|
||||||
|
duration?: number
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
if (action === 'share') {
|
if (action === 'share') {
|
||||||
const response = await apiCall('http://192.168.1.120:8082/api/share', {
|
const response = await apiCall('/api/share', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
file_id: fileId,
|
file_id: fileId,
|
||||||
expires_in_days: 7
|
expires_in_days: duration || 7
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const shareId = await response.json();
|
const shareId = await response.json();
|
||||||
setSuccess(`Share link created: /shared/${shareId}`);
|
setSuccess(`Share link created: /shared/${shareId}`);
|
||||||
|
|
||||||
} else if (action === 'delete') {
|
} else if (action === 'delete') {
|
||||||
await apiCall(`http://192.168.1.120:8082/api/files/${fileId}`, {
|
await apiCall(`/api/files/${fileId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
setSuccess('File deleted successfully');
|
setSuccess('File deleted successfully');
|
||||||
|
@ -147,9 +150,28 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
return new Date(dateString).toLocaleString();
|
const now = new Date();
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const diff = (now.getTime() - date.getTime()) / 1000;
|
||||||
|
|
||||||
|
const isSameDay = (d1: Date, d2: Date) =>
|
||||||
|
d1.getFullYear() === d2.getFullYear() &&
|
||||||
|
d1.getMonth() === d2.getMonth() &&
|
||||||
|
d1.getDate() === d2.getDate();
|
||||||
|
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(now.getDate() - 1);
|
||||||
|
|
||||||
|
if (diff < 10) return "now";
|
||||||
|
|
||||||
|
if (isSameDay(date, yesterday)) {
|
||||||
|
return `yesterday at ${date.toLocaleTimeString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const formatFileSize = (bytes: number) => {
|
const formatFileSize = (bytes: number) => {
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
|
@ -193,7 +215,6 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Upload size={18} />
|
<Upload size={18} />
|
||||||
Upload File
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -204,44 +225,47 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`table-container ${isDarkMode ? 'table-container-dark' : 'table-container-light'}`}>
|
<div className={`table-container ${isDarkMode ? 'table-container-dark' : 'table-container-light'}`}>
|
||||||
<table className="data-table">
|
<table className="data-table full-width-table">
|
||||||
<thead className={isDarkMode ? 'table-header-dark' : 'table-header-light'}>
|
<thead className={isDarkMode ? 'table-header-dark' : 'table-header-light'}>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="table-header-cell">Name</th>
|
<th className="table-header-cell">Name</th>
|
||||||
<th className="table-header-cell">Size</th>
|
<th className="table-header-cell">Size</th>
|
||||||
<th className="table-header-cell">Uploaded</th>
|
<th className="table-header-cell">Uploaded</th>
|
||||||
<th className="table-header-cell">Actions</th>
|
<th className="table-header-cell actions-header-cell"></th> {/* Nessuna scritta */}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className={`table-body ${isDarkMode ? 'table-body-dark' : 'table-body-light'}`}>
|
<tbody className={`table-body ${isDarkMode ? 'table-body-dark' : 'table-body-light'}`}>
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<tr key={file.id} className={isDarkMode ? 'table-row-dark' : 'table-row-light'}>
|
<tr key={file.id} className={isDarkMode ? 'table-row-dark' : 'table-row-light'}>
|
||||||
<td className={`table-cell ${isDarkMode ? 'text-white' : 'text-gray-900'}`}>
|
<td className={`table-cell ${isDarkMode ? 'text-white' : 'text-gray-900'}`}>{file.original_name}</td>
|
||||||
{file.original_name}
|
<td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>{formatFileSize(file.size)}</td>
|
||||||
</td>
|
<td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>{formatDate(file.uploaded_at)}</td>
|
||||||
<td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>
|
<td className="table-cell relative actions-cell" ref={dropdownRef}>
|
||||||
{formatFileSize(file.size)}
|
|
||||||
</td>
|
|
||||||
<td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>
|
|
||||||
{formatDate(file.uploaded_at)}
|
|
||||||
</td>
|
|
||||||
<td className="table-cell relative" ref={dropdownRef}>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpenDropdown(openDropdown === file.id ? null : file.id)}
|
onClick={() => setOpenDropdown(openDropdown === file.id ? null : file.id)}
|
||||||
className={`actions-button ${
|
className={`actions-button ellipsis-button ${isDarkMode ? 'actions-button-dark' : 'actions-button-light'}`}
|
||||||
isDarkMode ? 'actions-button-dark' : 'actions-button-light'
|
aria-label="Actions"
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Actions
|
<span className="ellipsis-dot"></span>
|
||||||
<ChevronDown size={16} />
|
<span className="ellipsis-dot"></span>
|
||||||
|
<span className="ellipsis-dot"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{openDropdown === file.id && (
|
{openDropdown === file.id && (
|
||||||
<div className={`dropdown-menu ${
|
<div className={`dropdown-menu ${isDarkMode ? 'dropdown-menu-dark' : 'dropdown-menu-light'} custom-dropdown`}>
|
||||||
isDarkMode ? 'dropdown-menu-dark' : 'dropdown-menu-light'
|
<div className="dropdown-section">
|
||||||
}`}>
|
<label className="dropdown-label">Share duration:</label>
|
||||||
|
<select
|
||||||
|
className="dropdown-select"
|
||||||
|
value={shareDuration}
|
||||||
|
onChange={e => setShareDuration(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value={1}>1 day</option>
|
||||||
|
<option value={3}>3 days</option>
|
||||||
|
<option value={7}>7 days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFileAction('share', file.id)}
|
onClick={() => handleFileAction('share', file.id, shareDuration)}
|
||||||
className={`dropdown-item ${
|
className={`dropdown-item ${
|
||||||
isDarkMode ? 'dropdown-item-dark' : 'dropdown-item-light'
|
isDarkMode ? 'dropdown-item-dark' : 'dropdown-item-light'
|
||||||
}`}
|
}`}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const Login: React.FC<LoginProps> = ({
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("http://192.168.1.120:8082/api/login", {
|
const res = await fetch("/api/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
|
|
|
@ -32,6 +32,7 @@ export const MainPage: React.FC<MainPageProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderActiveView = () => {
|
const renderActiveView = () => {
|
||||||
|
console.log('isDarkMode:', isDarkMode);
|
||||||
switch (activeView) {
|
switch (activeView) {
|
||||||
case 'files':
|
case 'files':
|
||||||
return <FilesComponent isDarkMode={isDarkMode} />;
|
return <FilesComponent isDarkMode={isDarkMode} />;
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const Register: React.FC<RegisterProps> = ({
|
||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("http://localhost:8000/api/register", {
|
const res = await fetch("/api/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const SharesComponent: React.FC<SharesComponentProps> = ({ isDarkMode })
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
|
||||||
const getAuthToken = () => {
|
const getAuthToken = () => {
|
||||||
return localStorage.getItem('authToken') || '';
|
return localStorage.getItem('litecloud_token') || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiCall = async (url: string, options: RequestInit = {}) => {
|
const apiCall = async (url: string, options: RequestInit = {}) => {
|
||||||
|
|
|
@ -633,6 +633,103 @@ body, html, #root {
|
||||||
background: rgba(254, 226, 226, 1);
|
background: rgba(254, 226, 226, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dropdown menu custom style for FileComponent */
|
||||||
|
.custom-dropdown {
|
||||||
|
background: rgba(40, 40, 60, 0.85);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.25);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: 100;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 110%;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-light.custom-dropdown {
|
||||||
|
background: rgba(255,255,255,0.95);
|
||||||
|
color: #222;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item-dark {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item-light {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: rgba(147, 51, 234, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item-danger {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item-danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-label {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-select {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
background: #fff;
|
||||||
|
color: #222;
|
||||||
|
outline: none;
|
||||||
|
transition: border 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-dark .dropdown-select {
|
||||||
|
background: #23233a;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-dark .dropdown-label {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-light .dropdown-label {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
/* Upload Button */
|
/* Upload Button */
|
||||||
.upload-button {
|
.upload-button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -654,6 +751,22 @@ body, html, #root {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dark .sidebar-title {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-light .sidebar-title {
|
||||||
|
color: #1f2937; /* gray-800 */
|
||||||
|
}
|
||||||
|
|
||||||
.upload-button:hover {
|
.upload-button:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
|
@ -696,6 +809,54 @@ body, html, #root {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ellipsis button (3 dots) style */
|
||||||
|
.ellipsis-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.ellipsis-button:hover {
|
||||||
|
background: rgba(147, 51, 234, 0.08);
|
||||||
|
}
|
||||||
|
.ellipsis-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: #bdbdbd;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 1.5px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.actions-button-dark .ellipsis-dot {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.actions-button-light .ellipsis-dot {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Migliora la larghezza delle colonne e padding */
|
||||||
|
.full-width-table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
.table-header-cell, .table-cell {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
.actions-header-cell {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
.actions-cell {
|
||||||
|
text-align: right;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.app-layout {
|
.app-layout {
|
||||||
|
|
Loading…
Reference in a new issue