fix: Improve TOTP reliability, move components around, turn homepage update log into a sheet
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TermixAlert } from "../../../types/index.js";
|
||||
import type { TermixAlert } from "../../../../../../types";
|
||||
|
||||
interface AlertCardProps {
|
||||
alert: TermixAlert;
|
||||
@@ -67,7 +67,7 @@ const getTypeBadgeVariant = (type?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export function HomepageAlertCard({
|
||||
export function AlertCard({
|
||||
alert,
|
||||
onDismiss,
|
||||
onClose,
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { HomepageAlertCard } from "./HomepageAlertCard.tsx";
|
||||
import { AlertCard } from "./AlertCard.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { getUserAlerts, dismissAlert } from "@/ui/main-axios.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TermixAlert } from "../../../types/index.js";
|
||||
import type { TermixAlert } from "../../../../../../types";
|
||||
|
||||
interface AlertManagerProps {
|
||||
userId: string | null;
|
||||
loggedIn: boolean;
|
||||
}
|
||||
|
||||
export function HomepageAlertManager({
|
||||
export function AlertManager({
|
||||
userId,
|
||||
loggedIn,
|
||||
}: AlertManagerProps): React.ReactElement {
|
||||
@@ -129,7 +129,7 @@ export function HomepageAlertManager({
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-[99999]">
|
||||
<div className="relative w-full max-w-2xl mx-4">
|
||||
<HomepageAlertCard
|
||||
<AlertCard
|
||||
alert={currentAlert}
|
||||
onDismiss={handleDismissAlert}
|
||||
onClose={handleCloseCurrentAlert}
|
||||
218
src/ui/Desktop/Apps/Homepage/Apps/UpdateLog.tsx
Normal file
218
src/ui/Desktop/Apps/Homepage/Apps/UpdateLog.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
} from "@/components/ui/sheet.tsx";
|
||||
import { getReleasesRSS, getVersionInfo } from "@/ui/main-axios.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BookOpen, X } from "lucide-react";
|
||||
|
||||
interface HomepageUpdateLogProps extends React.ComponentProps<"div"> {
|
||||
loggedIn: boolean;
|
||||
}
|
||||
|
||||
interface ReleaseItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
pubDate: string;
|
||||
version: string;
|
||||
isPrerelease: boolean;
|
||||
isDraft: boolean;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
size: number;
|
||||
download_count: number;
|
||||
download_url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RSSResponse {
|
||||
feed: {
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
updated: string;
|
||||
};
|
||||
items: ReleaseItem[];
|
||||
total_count: number;
|
||||
cached: boolean;
|
||||
cache_age?: number;
|
||||
}
|
||||
|
||||
interface VersionResponse {
|
||||
status: "up_to_date" | "requires_update";
|
||||
version: string;
|
||||
latest_release: {
|
||||
name: string;
|
||||
published_at: string;
|
||||
html_url: string;
|
||||
};
|
||||
cached: boolean;
|
||||
cache_age?: number;
|
||||
}
|
||||
|
||||
export function HomepageUpdateLog({ loggedIn }: HomepageUpdateLogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [releases, setReleases] = useState<RSSResponse | null>(null);
|
||||
const [versionInfo, setVersionInfo] = useState<VersionResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (loggedIn && isOpen) {
|
||||
setLoading(true);
|
||||
Promise.all([getReleasesRSS(100), getVersionInfo()])
|
||||
.then(([releasesRes, versionRes]) => {
|
||||
setReleases(releasesRes);
|
||||
setVersionInfo(versionRes);
|
||||
setError(null);
|
||||
})
|
||||
.catch(() => {
|
||||
setError(t("common.failedToFetchUpdateInfo"));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [loggedIn, isOpen]);
|
||||
|
||||
if (!loggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatDescription = (description: string) => {
|
||||
const firstLine = description.split("\n")[0];
|
||||
return firstLine.replace(/[#*`]/g, "").replace(/\s+/g, " ").trim();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="text-sm border-2 border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg-darker transition-colors"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<BookOpen className="w-4 h-4 mr-2" />
|
||||
{t("common.updatesAndReleases")}
|
||||
</Button>
|
||||
|
||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-[500px] bg-dark-bg border-l-2 border-dark-border text-white sm:max-w-[500px] p-0 flex flex-col [&>button]:hidden"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-dark-border">
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
{t("common.updatesAndReleases")}
|
||||
</h2>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-white transition-colors flex items-center justify-center"
|
||||
title={t("common.close")}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{versionInfo && versionInfo.status === "requires_update" && (
|
||||
<Alert className="bg-dark-bg-darker border-dark-border text-white mb-3">
|
||||
<AlertTitle className="text-white">
|
||||
{t("common.updateAvailable")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-gray-300">
|
||||
{t("common.newVersionAvailable", {
|
||||
version: versionInfo.version,
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
variant="destructive"
|
||||
className="bg-red-900/20 border-red-500 text-red-300 mb-3"
|
||||
>
|
||||
<AlertTitle className="text-red-300">
|
||||
{t("common.error")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-red-300">
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{releases?.items.map((release) => (
|
||||
<div
|
||||
key={release.id}
|
||||
className="border border-dark-border rounded-lg p-3 hover:bg-dark-bg-darker transition-colors cursor-pointer bg-dark-bg-darker/50"
|
||||
onClick={() => window.open(release.link, "_blank")}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="font-semibold text-sm leading-tight flex-1 text-white">
|
||||
{release.title}
|
||||
</h4>
|
||||
{release.isPrerelease && (
|
||||
<span className="text-xs bg-yellow-600 text-yellow-100 px-2 py-1 rounded ml-2 flex-shrink-0 font-medium">
|
||||
{t("common.preRelease")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-300 mb-2 leading-relaxed">
|
||||
{formatDescription(release.description)}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center text-xs text-gray-400">
|
||||
<span>
|
||||
{new Date(release.pubDate).toLocaleDateString()}
|
||||
</span>
|
||||
{release.assets.length > 0 && (
|
||||
<>
|
||||
<span className="mx-2">•</span>
|
||||
<span>
|
||||
{release.assets.length} asset
|
||||
{release.assets.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{releases && releases.items.length === 0 && !loading && (
|
||||
<Alert className="bg-dark-bg-darker border-dark-border text-gray-300">
|
||||
<AlertTitle className="text-gray-300">
|
||||
{t("common.noReleases")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-gray-400">
|
||||
{t("common.noReleasesFound")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { HomepageAuth } from "@/ui/Desktop/Homepage/HomepageAuth.tsx";
|
||||
import { HomepageUpdateLog } from "@/ui/Desktop/Homepage/HompageUpdateLog.tsx";
|
||||
import { HomepageAlertManager } from "@/ui/Desktop/Homepage/HomepageAlertManager.tsx";
|
||||
import { Auth } from "@/ui/Desktop/Authentication/Auth.tsx";
|
||||
import { HomepageUpdateLog } from "@/ui/Desktop/Apps/Homepage/Apps/UpdateLog.tsx";
|
||||
import { AlertManager } from "@/ui/Desktop/Apps/Homepage/Apps/Alerts/AlertManager.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { getUserInfo, getDatabaseHealth, getCookie } from "@/ui/main-axios.ts";
|
||||
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||||
|
||||
interface HomepageProps {
|
||||
onSelectView: (view: string) => void;
|
||||
@@ -29,8 +30,14 @@ export function Homepage({
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [dbError, setDbError] = useState<string | null>(null);
|
||||
|
||||
let sidebarState: "expanded" | "collapsed" = "expanded";
|
||||
try {
|
||||
const sidebar = useSidebar();
|
||||
sidebarState = sidebar.state;
|
||||
} catch {}
|
||||
|
||||
const topMarginPx = isTopbarOpen ? 74 : 26;
|
||||
const leftMarginPx = 26;
|
||||
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
|
||||
const bottomMarginPx = 8;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -80,7 +87,7 @@ export function Homepage({
|
||||
<>
|
||||
{!loggedIn ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<HomepageAuth
|
||||
<Auth
|
||||
setLoggedIn={setLoggedIn}
|
||||
setIsAdmin={setIsAdmin}
|
||||
setUsername={setUsername}
|
||||
@@ -94,7 +101,7 @@ export function Homepage({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: leftMarginPx,
|
||||
marginRight: 17,
|
||||
@@ -103,67 +110,62 @@ export function Homepage({
|
||||
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-center gap-8 relative z-10">
|
||||
<div className="flex flex-col items-center gap-6 w-[400px]">
|
||||
<HomepageUpdateLog loggedIn={loggedIn} />
|
||||
<div className="flex flex-col items-center justify-center gap-6 relative z-10">
|
||||
<HomepageUpdateLog loggedIn={loggedIn} />
|
||||
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() =>
|
||||
window.open("https://github.com/Termix-SSH/Termix", "_blank")
|
||||
}
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://github.com/Termix-SSH/Termix/issues/new",
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
>
|
||||
Feedback
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://discord.com/invite/jVQGdvHDrf",
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
>
|
||||
Discord
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() =>
|
||||
window.open("https://github.com/sponsors/LukeGus", "_blank")
|
||||
}
|
||||
>
|
||||
Donate
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-3 flex-wrap justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() =>
|
||||
window.open("https://github.com/Termix-SSH/Termix", "_blank")
|
||||
}
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://github.com/Termix-SSH/Termix/issues/new",
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
>
|
||||
Feedback
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() =>
|
||||
window.open("https://discord.com/invite/jVQGdvHDrf", "_blank")
|
||||
}
|
||||
>
|
||||
Discord
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() =>
|
||||
window.open("https://github.com/sponsors/LukeGus", "_blank")
|
||||
}
|
||||
>
|
||||
Donate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<HomepageAlertManager userId={userId} loggedIn={loggedIn} />
|
||||
<AlertManager userId={userId} loggedIn={loggedIn} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -308,6 +308,7 @@ export function Server({
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={isRefreshing}
|
||||
className="font-semibold"
|
||||
onClick={async () => {
|
||||
if (currentHostConfig?.id) {
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Plus, Play, Edit, Trash2, Copy } from "lucide-react";
|
||||
import { Plus, Play, Edit, Trash2, Copy, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
@@ -177,7 +177,7 @@ export function SnippetsSidebar({
|
||||
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-white transition-colors flex items-center justify-center"
|
||||
title={t("common.close")}
|
||||
>
|
||||
<span className="text-lg font-bold leading-none">×</span>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
) {
|
||||
ws.addEventListener("open", () => {
|
||||
connectionTimeoutRef.current = setTimeout(() => {
|
||||
if (!isConnected) {
|
||||
if (!isConnected && !totpRequired) {
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
@@ -482,6 +482,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
} else if (msg.type === "totp_required") {
|
||||
setTotpRequired(true);
|
||||
setTotpPrompt(msg.prompt || "Verification code:");
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toast.error(t("terminal.messageParseError"));
|
||||
@@ -788,6 +792,13 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}}
|
||||
/>
|
||||
|
||||
<TOTPDialog
|
||||
isOpen={totpRequired}
|
||||
prompt={totpPrompt}
|
||||
onSubmit={handleTotpSubmit}
|
||||
onCancel={handleTotpCancel}
|
||||
/>
|
||||
|
||||
{isConnecting && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-dark-bg">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -796,13 +807,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TOTPDialog
|
||||
isOpen={totpRequired}
|
||||
prompt={totpPrompt}
|
||||
onSubmit={handleTotpSubmit}
|
||||
onCancel={handleTotpCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
isElectron,
|
||||
logoutUser,
|
||||
} from "../../main-axios.ts";
|
||||
import { ServerConfig as ServerConfigComponent } from "@/ui/Desktop/Electron Only/ServerConfig.tsx";
|
||||
import { ElectronServerConfig as ServerConfigComponent } from "@/ui/Desktop/Authentication/ElectronServerConfig.tsx";
|
||||
|
||||
interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
||||
setLoggedIn: (loggedIn: boolean) => void;
|
||||
@@ -40,7 +40,7 @@ interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export function HomepageAuth({
|
||||
export function Auth({
|
||||
className,
|
||||
setLoggedIn,
|
||||
setIsAdmin,
|
||||
@@ -18,7 +18,7 @@ interface ServerConfigProps {
|
||||
isFirstTime?: boolean;
|
||||
}
|
||||
|
||||
export function ServerConfig({
|
||||
export function ElectronServerConfig({
|
||||
onServerConfigured,
|
||||
onCancel,
|
||||
isFirstTime = false,
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { LeftSidebar } from "@/ui/Desktop/Navigation/LeftSidebar.tsx";
|
||||
import { Homepage } from "@/ui/Desktop/Homepage/Homepage.tsx";
|
||||
import { Homepage } from "@/ui/Desktop/Apps/Homepage/Homepage.tsx";
|
||||
import { AppView } from "@/ui/Desktop/Navigation/AppView.tsx";
|
||||
import { HostManager } from "@/ui/Desktop/Apps/Host Manager/HostManager.tsx";
|
||||
import {
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { getReleasesRSS, getVersionInfo } from "@/ui/main-axios.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface HomepageUpdateLogProps extends React.ComponentProps<"div"> {
|
||||
loggedIn: boolean;
|
||||
}
|
||||
|
||||
interface ReleaseItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
pubDate: string;
|
||||
version: string;
|
||||
isPrerelease: boolean;
|
||||
isDraft: boolean;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
size: number;
|
||||
download_count: number;
|
||||
download_url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RSSResponse {
|
||||
feed: {
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
updated: string;
|
||||
};
|
||||
items: ReleaseItem[];
|
||||
total_count: number;
|
||||
cached: boolean;
|
||||
cache_age?: number;
|
||||
}
|
||||
|
||||
interface VersionResponse {
|
||||
status: "up_to_date" | "requires_update";
|
||||
version: string;
|
||||
latest_release: {
|
||||
name: string;
|
||||
published_at: string;
|
||||
html_url: string;
|
||||
};
|
||||
cached: boolean;
|
||||
cache_age?: number;
|
||||
}
|
||||
|
||||
export function HomepageUpdateLog({ loggedIn }: HomepageUpdateLogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [releases, setReleases] = useState<RSSResponse | null>(null);
|
||||
const [versionInfo, setVersionInfo] = useState<VersionResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (loggedIn) {
|
||||
setLoading(true);
|
||||
Promise.all([getReleasesRSS(100), getVersionInfo()])
|
||||
.then(([releasesRes, versionRes]) => {
|
||||
setReleases(releasesRes);
|
||||
setVersionInfo(versionRes);
|
||||
setError(null);
|
||||
})
|
||||
.catch(() => {
|
||||
setError(t("common.failedToFetchUpdateInfo"));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [loggedIn]);
|
||||
|
||||
if (!loggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatDescription = (description: string) => {
|
||||
const firstLine = description.split("\n")[0];
|
||||
return firstLine.replace(/[#*`]/g, "").replace(/\s+/g, " ").trim();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[400px] h-[600px] flex flex-col border-2 border-dark-border rounded-lg bg-dark-bg p-4 shadow-lg">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-3 text-white">
|
||||
{t("common.updatesAndReleases")}
|
||||
</h3>
|
||||
|
||||
<Separator className="p-0.25 mt-3 mb-3 bg-dark-border" />
|
||||
|
||||
{versionInfo && versionInfo.status === "requires_update" && (
|
||||
<Alert className="bg-dark-bg-darker border-dark-border text-white">
|
||||
<AlertTitle className="text-white">
|
||||
{t("common.updateAvailable")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-gray-300">
|
||||
{t("common.newVersionAvailable", {
|
||||
version: versionInfo.version,
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{versionInfo && versionInfo.status === "requires_update" && (
|
||||
<Separator className="p-0.25 mt-3 mb-3 bg-dark-border" />
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-3 pr-2">
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
variant="destructive"
|
||||
className="bg-red-900/20 border-red-500 text-red-300"
|
||||
>
|
||||
<AlertTitle className="text-red-300">
|
||||
{t("common.error")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-red-300">
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{releases?.items.map((release) => (
|
||||
<div
|
||||
key={release.id}
|
||||
className="border border-dark-border rounded-lg p-3 hover:bg-dark-bg-darker transition-colors cursor-pointer bg-dark-bg-darker/50"
|
||||
onClick={() => window.open(release.link, "_blank")}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="font-semibold text-sm leading-tight flex-1 text-white">
|
||||
{release.title}
|
||||
</h4>
|
||||
{release.isPrerelease && (
|
||||
<span className="text-xs bg-yellow-600 text-yellow-100 px-2 py-1 rounded ml-2 flex-shrink-0 font-medium">
|
||||
{t("common.preRelease")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-300 mb-2 leading-relaxed">
|
||||
{formatDescription(release.description)}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center text-xs text-gray-400">
|
||||
<span>{new Date(release.pubDate).toLocaleDateString()}</span>
|
||||
{release.assets.length > 0 && (
|
||||
<>
|
||||
<span className="mx-2">•</span>
|
||||
<span>
|
||||
{release.assets.length} asset
|
||||
{release.assets.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{releases && releases.items.length === 0 && !loading && (
|
||||
<Alert className="bg-dark-bg-darker border-dark-border text-gray-300">
|
||||
<AlertTitle className="text-gray-300">
|
||||
{t("common.noReleases")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-gray-400">
|
||||
{t("common.noReleasesFound")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,8 +23,8 @@ export function TOTPDialog({
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center z-50">
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-50">
|
||||
<div className="absolute inset-0 bg-dark-bg rounded-md" />
|
||||
<div className="bg-dark-bg border-2 border-dark-border rounded-lg p-6 max-w-md w-full mx-4 relative z-10">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 text-primary" />
|
||||
|
||||
Reference in New Issue
Block a user