mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 20:46:44 +03:00
feat: add skeleton loaders for improved loading state in Dashboard and Header components
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from '@/components/ui/card';
|
||||||
import { GitHubConfigForm } from "./GitHubConfigForm";
|
import { GitHubConfigForm } from './GitHubConfigForm';
|
||||||
import { GiteaConfigForm } from "./GiteaConfigForm";
|
import { GiteaConfigForm } from './GiteaConfigForm';
|
||||||
import { ScheduleConfigForm } from "./ScheduleConfigForm";
|
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
||||||
import type {
|
import type {
|
||||||
ConfigApiResponse,
|
ConfigApiResponse,
|
||||||
GiteaConfig,
|
GiteaConfig,
|
||||||
@@ -16,12 +16,13 @@ import type {
|
|||||||
SaveConfigApiRequest,
|
SaveConfigApiRequest,
|
||||||
SaveConfigApiResponse,
|
SaveConfigApiResponse,
|
||||||
ScheduleConfig,
|
ScheduleConfig,
|
||||||
} from "@/types/config";
|
} from '@/types/config';
|
||||||
import { Button } from "../ui/button";
|
import { Button } from '../ui/button';
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { apiRequest } from "@/lib/utils";
|
import { apiRequest } from '@/lib/utils';
|
||||||
import { Copy, CopyCheck, RefreshCw } from "lucide-react";
|
import { Copy, CopyCheck, RefreshCw } from 'lucide-react';
|
||||||
import { toast } from "sonner";
|
import { toast } from 'sonner';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
type ConfigState = {
|
type ConfigState = {
|
||||||
githubConfig: GitHubConfig;
|
githubConfig: GitHubConfig;
|
||||||
@@ -32,8 +33,8 @@ type ConfigState = {
|
|||||||
export function ConfigTabs() {
|
export function ConfigTabs() {
|
||||||
const [config, setConfig] = useState<ConfigState>({
|
const [config, setConfig] = useState<ConfigState>({
|
||||||
githubConfig: {
|
githubConfig: {
|
||||||
username: "",
|
username: '',
|
||||||
token: "",
|
token: '',
|
||||||
skipForks: false,
|
skipForks: false,
|
||||||
privateRepositories: false,
|
privateRepositories: false,
|
||||||
mirrorIssues: false,
|
mirrorIssues: false,
|
||||||
@@ -41,16 +42,14 @@ export function ConfigTabs() {
|
|||||||
preserveOrgStructure: false,
|
preserveOrgStructure: false,
|
||||||
skipStarredIssues: false,
|
skipStarredIssues: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
url: "",
|
url: '',
|
||||||
username: "",
|
username: '',
|
||||||
token: "",
|
token: '',
|
||||||
organization: "github-mirrors",
|
organization: 'github-mirrors',
|
||||||
visibility: "public",
|
visibility: 'public',
|
||||||
starredReposOrg: "github",
|
starredReposOrg: 'github',
|
||||||
},
|
},
|
||||||
|
|
||||||
scheduleConfig: {
|
scheduleConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
interval: 3600,
|
interval: 3600,
|
||||||
@@ -58,27 +57,21 @@ export function ConfigTabs() {
|
|||||||
});
|
});
|
||||||
const { user, refreshUser } = useAuth();
|
const { user, refreshUser } = useAuth();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [dockerCode, setDockerCode] = useState<string>("");
|
const [dockerCode, setDockerCode] = useState<string>('');
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||||
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
||||||
const [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
|
const [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
|
||||||
|
|
||||||
// Check if all required fields are filled to enable the Save Configuration button
|
|
||||||
const isConfigFormValid = (): boolean => {
|
const isConfigFormValid = (): boolean => {
|
||||||
const { githubConfig, giteaConfig } = config;
|
const { githubConfig, giteaConfig } = config;
|
||||||
|
|
||||||
// Check GitHub required fields
|
|
||||||
const isGitHubValid = !!(
|
const isGitHubValid = !!(
|
||||||
githubConfig.username?.trim() && githubConfig.token?.trim()
|
githubConfig.username.trim() && githubConfig.token.trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check Gitea required fields
|
|
||||||
const isGiteaValid = !!(
|
const isGiteaValid = !!(
|
||||||
giteaConfig.url?.trim() &&
|
giteaConfig.url.trim() &&
|
||||||
giteaConfig.username?.trim() &&
|
giteaConfig.username.trim() &&
|
||||||
giteaConfig.token?.trim()
|
giteaConfig.token.trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
return isGitHubValid && isGiteaValid;
|
return isGitHubValid && isGiteaValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,11 +79,12 @@ export function ConfigTabs() {
|
|||||||
const updateLastAndNextRun = () => {
|
const updateLastAndNextRun = () => {
|
||||||
const lastRun = config.scheduleConfig.lastRun
|
const lastRun = config.scheduleConfig.lastRun
|
||||||
? new Date(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 intervalInSeconds = config.scheduleConfig.interval;
|
||||||
const nextRun = new Date(lastRun.getTime() + intervalInSeconds * 1000);
|
const nextRun = new Date(
|
||||||
|
lastRun.getTime() + intervalInSeconds * 1000,
|
||||||
setConfig((prev) => ({
|
);
|
||||||
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
scheduleConfig: {
|
scheduleConfig: {
|
||||||
...prev.scheduleConfig,
|
...prev.scheduleConfig,
|
||||||
@@ -99,37 +93,31 @@ export function ConfigTabs() {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
updateLastAndNextRun();
|
updateLastAndNextRun();
|
||||||
}, [config.scheduleConfig.interval]);
|
}, [config.scheduleConfig.interval]);
|
||||||
|
|
||||||
const handleImportGitHubData = async () => {
|
const handleImportGitHubData = async () => {
|
||||||
try {
|
|
||||||
if (!user?.id) return;
|
if (!user?.id) return;
|
||||||
|
|
||||||
setIsSyncing(true);
|
setIsSyncing(true);
|
||||||
|
try {
|
||||||
const result = await apiRequest<{ success: boolean; message?: string }>(
|
const result = await apiRequest<{ success: boolean; message?: string }>(
|
||||||
`/sync?userId=${user.id}`,
|
`/sync?userId=${user.id}`,
|
||||||
{
|
{ method: 'POST' },
|
||||||
method: "POST",
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
result.success
|
||||||
if (result.success) {
|
? toast.success(
|
||||||
toast.success(
|
'GitHub data imported successfully! Head to the Dashboard to start mirroring repositories.',
|
||||||
"GitHub data imported successfully! Head to the Dashboard to start mirroring repositories."
|
)
|
||||||
|
: toast.error(
|
||||||
|
`Failed to import GitHub data: ${
|
||||||
|
result.message || 'Unknown error'
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
`Failed to import GitHub data: ${result.message || "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Error importing GitHub data: ${
|
`Error importing GitHub data: ${
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
}`
|
}`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSyncing(false);
|
setIsSyncing(false);
|
||||||
@@ -137,94 +125,76 @@ export function ConfigTabs() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveConfig = async () => {
|
const handleSaveConfig = async () => {
|
||||||
try {
|
if (!user?.id) return;
|
||||||
if (!user || !user.id) {
|
const reqPayload: SaveConfigApiRequest = {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reqPyload: SaveConfigApiRequest = {
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
githubConfig: config.githubConfig,
|
githubConfig: config.githubConfig,
|
||||||
giteaConfig: config.giteaConfig,
|
giteaConfig: config.giteaConfig,
|
||||||
scheduleConfig: config.scheduleConfig,
|
scheduleConfig: config.scheduleConfig,
|
||||||
};
|
};
|
||||||
const response = await fetch("/api/config", {
|
try {
|
||||||
method: "POST",
|
const response = await fetch('/api/config', {
|
||||||
headers: {
|
method: 'POST',
|
||||||
"Content-Type": "application/json",
|
headers: { 'Content-Type': 'application/json' },
|
||||||
},
|
body: JSON.stringify(reqPayload),
|
||||||
body: JSON.stringify(reqPyload),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: SaveConfigApiResponse = await response.json();
|
const result: SaveConfigApiResponse = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
setIsConfigSaved(true);
|
setIsConfigSaved(true);
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
"Configuration saved successfully! Now import your GitHub data to begin."
|
'Configuration saved successfully! Now import your GitHub data to begin.',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Failed to save configuration: ${result.message || "Unknown error"}`
|
`Failed to save configuration: ${result.message || 'Unknown error'}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`An error occurred while saving the configuration: ${
|
`An error occurred while saving the configuration: ${
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
}`
|
}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
try {
|
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
const response = await apiRequest<ConfigApiResponse>(
|
const response = await apiRequest<ConfigApiResponse>(
|
||||||
`/config?userId=${user.id}`,
|
`/config?userId=${user.id}`,
|
||||||
{
|
{ method: 'GET' },
|
||||||
method: "GET",
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if we have a valid config response
|
|
||||||
if (response && !response.error) {
|
if (response && !response.error) {
|
||||||
setConfig({
|
setConfig({
|
||||||
githubConfig: response.githubConfig || config.githubConfig,
|
githubConfig:
|
||||||
giteaConfig: response.giteaConfig || config.giteaConfig,
|
response.githubConfig || config.githubConfig,
|
||||||
scheduleConfig: response.scheduleConfig || config.scheduleConfig,
|
giteaConfig:
|
||||||
|
response.giteaConfig || config.giteaConfig,
|
||||||
|
scheduleConfig:
|
||||||
|
response.scheduleConfig || config.scheduleConfig,
|
||||||
});
|
});
|
||||||
|
if (response.id) setIsConfigSaved(true);
|
||||||
// If we got a valid config from the server, it means it was previously saved
|
|
||||||
if (response.id) {
|
|
||||||
setIsConfigSaved(true);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// If there's an error, we'll just use the default config defined in state
|
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Don't show error for first-time users, just use the default config
|
console.warn(
|
||||||
console.warn("Could not fetch configuration, using defaults:", error);
|
'Could not fetch configuration, using defaults:',
|
||||||
} finally {
|
error,
|
||||||
setIsLoading(false);
|
);
|
||||||
}
|
}
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const generateDockerCode = () => {
|
const generateDockerCode = () => `
|
||||||
return `services:
|
services:
|
||||||
gitea-mirror:
|
gitea-mirror:
|
||||||
image: arunavo4/gitea-mirror:latest
|
image: arunavo4/gitea-mirror:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -243,27 +213,93 @@ export function ConfigTabs() {
|
|||||||
- GITEA_ORGANIZATION=${config.giteaConfig.organization}
|
- GITEA_ORGANIZATION=${config.giteaConfig.organization}
|
||||||
- GITEA_ORG_VISIBILITY=${config.giteaConfig.visibility}
|
- GITEA_ORG_VISIBILITY=${config.giteaConfig.visibility}
|
||||||
- DELAY=${config.scheduleConfig.interval}`;
|
- DELAY=${config.scheduleConfig.interval}`;
|
||||||
};
|
setDockerCode(generateDockerCode());
|
||||||
|
|
||||||
const code = generateDockerCode();
|
|
||||||
setDockerCode(code);
|
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const handleCopyToClipboard = (text: string) => {
|
const handleCopyToClipboard = (text: string) => {
|
||||||
navigator.clipboard.writeText(text).then(
|
navigator.clipboard.writeText(text).then(
|
||||||
() => {
|
() => {
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
toast.success("Docker configuration copied to clipboard!");
|
toast.success('Docker configuration copied to clipboard!');
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
},
|
},
|
||||||
(err) => {
|
() => toast.error('Could not copy text to clipboard.'),
|
||||||
toast.error("Could not copy text to clipboard.");
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function ConfigCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex-row justify-between">
|
||||||
|
<div className="flex flex-col gap-y-1.5 m-0">
|
||||||
|
<Skeleton className="h-6 w-48" />
|
||||||
|
<Skeleton className="h-4 w-72" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-x-4">
|
||||||
|
<Skeleton className="h-10 w-36" />
|
||||||
|
<Skeleton className="h-10 w-36" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="flex gap-x-4">
|
||||||
|
<div className="w-1/2 border rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<Skeleton className="h-6 w-40" />
|
||||||
|
<Skeleton className="h-9 w-32" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-20 w-full" />
|
||||||
|
<Skeleton className="h-20 w-full" />
|
||||||
|
<Skeleton className="h-32 w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2 border rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<Skeleton className="h-6 w-40" />
|
||||||
|
<Skeleton className="h-9 w-32" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-20 w-full" />
|
||||||
|
<Skeleton className="h-20 w-full" />
|
||||||
|
<Skeleton className="h-20 w-full" />
|
||||||
|
<Skeleton className="h-20 w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-8 w-48" />
|
||||||
|
<Skeleton className="h-16 w-full" />
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DockerConfigSkeleton() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Skeleton className="h-6 w-40" />
|
||||||
|
<Skeleton className="h-4 w-64" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="relative">
|
||||||
|
<Skeleton className="h-8 w-8 absolute top-4 right-10 rounded-md" />
|
||||||
|
<Skeleton className="h-48 w-full rounded-md" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<div>loading...</div>
|
<div className="flex flex-col gap-y-6">
|
||||||
|
<ConfigCardSkeleton />
|
||||||
|
<DockerConfigSkeleton />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-y-6">
|
<div className="flex flex-col gap-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -275,17 +311,16 @@ export function ConfigTabs() {
|
|||||||
mirroring.
|
mirroring.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-x-4">
|
<div className="flex gap-x-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleImportGitHubData}
|
onClick={handleImportGitHubData}
|
||||||
disabled={isSyncing || !isConfigSaved}
|
disabled={isSyncing || !isConfigSaved}
|
||||||
title={
|
title={
|
||||||
!isConfigSaved
|
!isConfigSaved
|
||||||
? "Save configuration first"
|
? 'Save configuration first'
|
||||||
: isSyncing
|
: isSyncing
|
||||||
? "Import in progress"
|
? 'Import in progress'
|
||||||
: "Import GitHub Data"
|
: 'Import GitHub Data'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isSyncing ? (
|
{isSyncing ? (
|
||||||
@@ -305,66 +340,57 @@ export function ConfigTabs() {
|
|||||||
disabled={!isConfigFormValid()}
|
disabled={!isConfigFormValid()}
|
||||||
title={
|
title={
|
||||||
!isConfigFormValid()
|
!isConfigFormValid()
|
||||||
? "Please fill all required fields"
|
? 'Please fill all required fields'
|
||||||
: "Save Configuration"
|
: 'Save Configuration'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Save Configuration
|
Save Configuration
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<div className="flex gap-x-4">
|
<div className="flex gap-x-4">
|
||||||
<GitHubConfigForm
|
<GitHubConfigForm
|
||||||
config={config.githubConfig}
|
config={config.githubConfig}
|
||||||
setConfig={(update) =>
|
setConfig={update =>
|
||||||
setConfig((prev) => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
githubConfig:
|
githubConfig:
|
||||||
typeof update === "function"
|
typeof update === 'function'
|
||||||
? update(prev.githubConfig)
|
? update(prev.githubConfig)
|
||||||
: update,
|
: update,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GiteaConfigForm
|
<GiteaConfigForm
|
||||||
config={config?.giteaConfig ?? ({} as GiteaConfig)}
|
config={config.giteaConfig}
|
||||||
setConfig={(update) =>
|
setConfig={update =>
|
||||||
setConfig((prev) => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
giteaConfig:
|
giteaConfig:
|
||||||
typeof update === "function"
|
typeof update === 'function'
|
||||||
? update(prev.giteaConfig)
|
? update(prev.giteaConfig)
|
||||||
: update,
|
: update,
|
||||||
githubConfig: prev?.githubConfig ?? ({} as GitHubConfig),
|
|
||||||
scheduleConfig:
|
|
||||||
prev?.scheduleConfig ?? ({} as ScheduleConfig),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScheduleConfigForm
|
<ScheduleConfigForm
|
||||||
config={config?.scheduleConfig ?? ({} as ScheduleConfig)}
|
config={config.scheduleConfig}
|
||||||
setConfig={(update) =>
|
setConfig={update =>
|
||||||
setConfig((prev) => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
scheduleConfig:
|
scheduleConfig:
|
||||||
typeof update === "function"
|
typeof update === 'function'
|
||||||
? update(prev.scheduleConfig)
|
? update(prev.scheduleConfig)
|
||||||
: update,
|
: update,
|
||||||
githubConfig: prev?.githubConfig ?? ({} as GitHubConfig),
|
|
||||||
giteaConfig: prev?.giteaConfig ?? ({} as GiteaConfig),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Docker Configuration</CardTitle>
|
<CardTitle>Docker Configuration</CardTitle>
|
||||||
@@ -372,7 +398,6 @@ export function ConfigTabs() {
|
|||||||
Equivalent Docker configuration for your current settings.
|
Equivalent Docker configuration for your current settings.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="relative">
|
<CardContent className="relative">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -386,7 +411,6 @@ export function ConfigTabs() {
|
|||||||
<Copy className="text-muted-foreground" />
|
<Copy className="text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<pre className="bg-muted p-4 rounded-md overflow-auto text-sm">
|
<pre className="bg-muted p-4 rounded-md overflow-auto text-sm">
|
||||||
{dockerCode}
|
{dockerCode}
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { apiRequest } from "@/lib/utils";
|
|||||||
import type { DashboardApiResponse } from "@/types/dashboard";
|
import type { DashboardApiResponse } from "@/types/dashboard";
|
||||||
import { useSSE } from "@/hooks/useSEE";
|
import { useSSE } from "@/hooks/useSEE";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -59,8 +61,6 @@ export function Dashboard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
const response = await apiRequest<DashboardApiResponse>(
|
const response = await apiRequest<DashboardApiResponse>(
|
||||||
`/dashboard?userId=${user.id}`,
|
`/dashboard?userId=${user.id}`,
|
||||||
{
|
{
|
||||||
@@ -93,8 +93,61 @@ export function Dashboard() {
|
|||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
// Status Card Skeleton component
|
||||||
|
function StatusCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
</CardTitle>
|
||||||
|
<Skeleton className="h-4 w-4 rounded-full" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Skeleton className="h-8 w-16 mb-1" />
|
||||||
|
<Skeleton className="h-3 w-32" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return isLoading || !connected ? (
|
return isLoading || !connected ? (
|
||||||
<div>loading...</div>
|
<div className="flex flex-col gap-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<StatusCardSkeleton />
|
||||||
|
<StatusCardSkeleton />
|
||||||
|
<StatusCardSkeleton />
|
||||||
|
<StatusCardSkeleton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-x-6 items-start">
|
||||||
|
{/* Repository List Skeleton */}
|
||||||
|
<div className="w-1/2 border rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<Skeleton className="h-6 w-32" />
|
||||||
|
<Skeleton className="h-9 w-24" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-16 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Activity Skeleton */}
|
||||||
|
<div className="w-1/2 border rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<Skeleton className="h-6 w-32" />
|
||||||
|
<Skeleton className="h-9 w-24" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-16 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-y-6">
|
<div className="flex flex-col gap-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { SiGitea } from "react-icons/si";
|
|||||||
import { ModeToggle } from "@/components/theme/ModeToggle";
|
import { ModeToggle } from "@/components/theme/ModeToggle";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout, isLoading } = useAuth();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
toast.success("Logged out successfully");
|
toast.success("Logged out successfully");
|
||||||
@@ -15,6 +16,16 @@ export function Header() {
|
|||||||
logout();
|
logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auth buttons skeleton loader
|
||||||
|
function AuthButtonsSkeleton() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Skeleton className="h-10 w-10 rounded-full" /> {/* Avatar placeholder */}
|
||||||
|
<Skeleton className="h-10 w-24" /> {/* Button placeholder */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-b bg-background">
|
<header className="border-b bg-background">
|
||||||
<div className="flex h-[4.5rem] items-center justify-between px-6">
|
<div className="flex h-[4.5rem] items-center justify-between px-6">
|
||||||
@@ -25,7 +36,10 @@ export function Header() {
|
|||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
{user ? (
|
|
||||||
|
{isLoading ? (
|
||||||
|
<AuthButtonsSkeleton />
|
||||||
|
) : user ? (
|
||||||
<>
|
<>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src="" alt="@shadcn" />
|
<AvatarImage src="" alt="@shadcn" />
|
||||||
|
|||||||
Reference in New Issue
Block a user