diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx index 4ad1427..852f267 100644 --- a/src/components/config/ConfigTabs.tsx +++ b/src/components/config/ConfigTabs.tsx @@ -1,14 +1,14 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { GitHubConfigForm } from "./GitHubConfigForm"; -import { GiteaConfigForm } from "./GiteaConfigForm"; -import { ScheduleConfigForm } from "./ScheduleConfigForm"; +} from '@/components/ui/card'; +import { GitHubConfigForm } from './GitHubConfigForm'; +import { GiteaConfigForm } from './GiteaConfigForm'; +import { ScheduleConfigForm } from './ScheduleConfigForm'; import type { ConfigApiResponse, GiteaConfig, @@ -16,12 +16,13 @@ import type { SaveConfigApiRequest, SaveConfigApiResponse, ScheduleConfig, -} from "@/types/config"; -import { Button } from "../ui/button"; -import { useAuth } from "@/hooks/useAuth"; -import { apiRequest } from "@/lib/utils"; -import { Copy, CopyCheck, RefreshCw } from "lucide-react"; -import { toast } from "sonner"; +} from '@/types/config'; +import { Button } from '../ui/button'; +import { useAuth } from '@/hooks/useAuth'; +import { apiRequest } from '@/lib/utils'; +import { Copy, CopyCheck, RefreshCw } from 'lucide-react'; +import { toast } from 'sonner'; +import { Skeleton } from '@/components/ui/skeleton'; type ConfigState = { githubConfig: GitHubConfig; @@ -32,8 +33,8 @@ type ConfigState = { export function ConfigTabs() { const [config, setConfig] = useState({ githubConfig: { - username: "", - token: "", + username: '', + token: '', skipForks: false, privateRepositories: false, mirrorIssues: false, @@ -41,16 +42,14 @@ export function ConfigTabs() { preserveOrgStructure: false, skipStarredIssues: false, }, - giteaConfig: { - url: "", - username: "", - token: "", - organization: "github-mirrors", - visibility: "public", - starredReposOrg: "github", + url: '', + username: '', + token: '', + organization: 'github-mirrors', + visibility: 'public', + starredReposOrg: 'github', }, - scheduleConfig: { enabled: false, interval: 3600, @@ -58,27 +57,21 @@ export function ConfigTabs() { }); const { user, refreshUser } = useAuth(); const [isLoading, setIsLoading] = useState(true); - const [dockerCode, setDockerCode] = useState(""); + const [dockerCode, setDockerCode] = useState(''); const [isCopied, setIsCopied] = useState(false); const [isSyncing, setIsSyncing] = useState(false); const [isConfigSaved, setIsConfigSaved] = useState(false); - // Check if all required fields are filled to enable the Save Configuration button const isConfigFormValid = (): boolean => { const { githubConfig, giteaConfig } = config; - - // Check GitHub required fields const isGitHubValid = !!( - githubConfig.username?.trim() && githubConfig.token?.trim() + githubConfig.username.trim() && githubConfig.token.trim() ); - - // Check Gitea required fields const isGiteaValid = !!( - giteaConfig.url?.trim() && - giteaConfig.username?.trim() && - giteaConfig.token?.trim() + giteaConfig.url.trim() && + giteaConfig.username.trim() && + giteaConfig.token.trim() ); - return isGitHubValid && isGiteaValid; }; @@ -86,11 +79,12 @@ export function ConfigTabs() { const updateLastAndNextRun = () => { const lastRun = config.scheduleConfig.lastRun ? new Date(config.scheduleConfig.lastRun) - : new Date(); // fallback to now if lastRun is null + : new Date(); const intervalInSeconds = config.scheduleConfig.interval; - const nextRun = new Date(lastRun.getTime() + intervalInSeconds * 1000); - - setConfig((prev) => ({ + const nextRun = new Date( + lastRun.getTime() + intervalInSeconds * 1000, + ); + setConfig(prev => ({ ...prev, scheduleConfig: { ...prev.scheduleConfig, @@ -99,37 +93,31 @@ export function ConfigTabs() { }, })); }; - updateLastAndNextRun(); }, [config.scheduleConfig.interval]); const handleImportGitHubData = async () => { + if (!user?.id) return; + setIsSyncing(true); try { - if (!user?.id) return; - - setIsSyncing(true); - const result = await apiRequest<{ success: boolean; message?: string }>( `/sync?userId=${user.id}`, - { - method: "POST", - } + { method: 'POST' }, ); - - if (result.success) { - toast.success( - "GitHub data imported successfully! Head to the Dashboard to start mirroring repositories." - ); - } else { - toast.error( - `Failed to import GitHub data: ${result.message || "Unknown error"}` - ); - } + result.success + ? toast.success( + 'GitHub data imported successfully! Head to the Dashboard to start mirroring repositories.', + ) + : toast.error( + `Failed to import GitHub data: ${ + result.message || 'Unknown error' + }`, + ); } catch (error) { toast.error( `Error importing GitHub data: ${ error instanceof Error ? error.message : String(error) - }` + }`, ); } finally { setIsSyncing(false); @@ -137,94 +125,76 @@ export function ConfigTabs() { }; const handleSaveConfig = async () => { + if (!user?.id) return; + const reqPayload: SaveConfigApiRequest = { + userId: user.id, + githubConfig: config.githubConfig, + giteaConfig: config.giteaConfig, + scheduleConfig: config.scheduleConfig, + }; try { - if (!user || !user.id) { - return; - } - - const reqPyload: SaveConfigApiRequest = { - userId: user.id, - githubConfig: config.githubConfig, - giteaConfig: config.giteaConfig, - scheduleConfig: config.scheduleConfig, - }; - const response = await fetch("/api/config", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(reqPyload), + const response = await fetch('/api/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(reqPayload), }); - const result: SaveConfigApiResponse = await response.json(); - if (result.success) { await refreshUser(); setIsConfigSaved(true); - toast.success( - "Configuration saved successfully! Now import your GitHub data to begin." + 'Configuration saved successfully! Now import your GitHub data to begin.', ); } else { toast.error( - `Failed to save configuration: ${result.message || "Unknown error"}` + `Failed to save configuration: ${result.message || 'Unknown error'}`, ); } } catch (error) { toast.error( `An error occurred while saving the configuration: ${ error instanceof Error ? error.message : String(error) - }` + }`, ); } }; useEffect(() => { + if (!user) return; + const fetchConfig = async () => { + setIsLoading(true); try { - if (!user) { - return; - } - - setIsLoading(true); - const response = await apiRequest( `/config?userId=${user.id}`, - { - method: "GET", - } + { method: 'GET' }, ); - - // Check if we have a valid config response if (response && !response.error) { setConfig({ - githubConfig: response.githubConfig || config.githubConfig, - giteaConfig: response.giteaConfig || config.giteaConfig, - scheduleConfig: response.scheduleConfig || config.scheduleConfig, + githubConfig: + response.githubConfig || config.githubConfig, + giteaConfig: + response.giteaConfig || config.giteaConfig, + scheduleConfig: + response.scheduleConfig || config.scheduleConfig, }); - - // If we got a valid config from the server, it means it was previously saved - if (response.id) { - setIsConfigSaved(true); - } + if (response.id) setIsConfigSaved(true); } - // If there's an error, we'll just use the default config defined in state - - setIsLoading(false); } catch (error) { - // Don't show error for first-time users, just use the default config - console.warn("Could not fetch configuration, using defaults:", error); - } finally { - setIsLoading(false); + console.warn( + 'Could not fetch configuration, using defaults:', + error, + ); } + setIsLoading(false); }; fetchConfig(); }, [user]); useEffect(() => { - const generateDockerCode = () => { - return `services: + const generateDockerCode = () => ` +services: gitea-mirror: image: arunavo4/gitea-mirror:latest restart: unless-stopped @@ -243,27 +213,93 @@ export function ConfigTabs() { - GITEA_ORGANIZATION=${config.giteaConfig.organization} - GITEA_ORG_VISIBILITY=${config.giteaConfig.visibility} - DELAY=${config.scheduleConfig.interval}`; - }; - - const code = generateDockerCode(); - setDockerCode(code); + setDockerCode(generateDockerCode()); }, [config]); const handleCopyToClipboard = (text: string) => { navigator.clipboard.writeText(text).then( () => { setIsCopied(true); - toast.success("Docker configuration copied to clipboard!"); + toast.success('Docker configuration copied to clipboard!'); setTimeout(() => setIsCopied(false), 2000); }, - (err) => { - toast.error("Could not copy text to clipboard."); - } + () => toast.error('Could not copy text to clipboard.'), ); }; + function ConfigCardSkeleton() { + return ( + + +
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + + +
+
+
+
+ + +
+
+ + + + +
+
+
+
+
+ + + +
+
+
+
+
+ ); + } + + function DockerConfigSkeleton() { + return ( + + + + + + + + + + + ); + } + return isLoading ? ( -
loading...
+
+ + +
) : (
@@ -275,17 +311,16 @@ export function ConfigTabs() { mirroring.
-
-
- setConfig((prev) => ({ + setConfig={update => + setConfig(prev => ({ ...prev, githubConfig: - typeof update === "function" + typeof update === 'function' ? update(prev.githubConfig) : update, })) } /> - - setConfig((prev) => ({ + config={config.giteaConfig} + setConfig={update => + setConfig(prev => ({ ...prev, giteaConfig: - typeof update === "function" + typeof update === 'function' ? update(prev.giteaConfig) : update, - githubConfig: prev?.githubConfig ?? ({} as GitHubConfig), - scheduleConfig: - prev?.scheduleConfig ?? ({} as ScheduleConfig), })) } />
- - setConfig((prev) => ({ + config={config.scheduleConfig} + setConfig={update => + setConfig(prev => ({ ...prev, scheduleConfig: - typeof update === "function" + typeof update === 'function' ? update(prev.scheduleConfig) : update, - githubConfig: prev?.githubConfig ?? ({} as GitHubConfig), - giteaConfig: prev?.giteaConfig ?? ({} as GiteaConfig), })) } />
- Docker Configuration @@ -372,7 +398,6 @@ export function ConfigTabs() { Equivalent Docker configuration for your current settings. - -
             {dockerCode}
           
diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index 41156dd..3028e0d 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -9,6 +9,8 @@ import { apiRequest } from "@/lib/utils"; import type { DashboardApiResponse } from "@/types/dashboard"; import { useSSE } from "@/hooks/useSEE"; import { toast } from "sonner"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; export function Dashboard() { const { user } = useAuth(); @@ -59,8 +61,6 @@ export function Dashboard() { return; } - setIsLoading(false); - const response = await apiRequest( `/dashboard?userId=${user.id}`, { @@ -93,8 +93,61 @@ export function Dashboard() { fetchDashboardData(); }, [user]); + // Status Card Skeleton component + function StatusCardSkeleton() { + return ( + + + + + + + + + + + + + ); + } + return isLoading || !connected ? ( -
loading...
+
+
+ + + + +
+ +
+ {/* Repository List Skeleton */} +
+
+ + +
+
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+
+ + {/* Recent Activity Skeleton */} +
+
+ + +
+
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+
+
+
) : (
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index f469f7e..83d1691 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -4,9 +4,10 @@ import { SiGitea } from "react-icons/si"; import { ModeToggle } from "@/components/theme/ModeToggle"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { toast } from "sonner"; +import { Skeleton } from "@/components/ui/skeleton"; export function Header() { - const { user, logout } = useAuth(); + const { user, logout, isLoading } = useAuth(); const handleLogout = async () => { toast.success("Logged out successfully"); @@ -15,6 +16,16 @@ export function Header() { logout(); }; + // Auth buttons skeleton loader + function AuthButtonsSkeleton() { + return ( + <> + {/* Avatar placeholder */} + {/* Button placeholder */} + + ); + } + return (
@@ -25,7 +36,10 @@ export function Header() {
- {user ? ( + + {isLoading ? ( + + ) : user ? ( <>