chore(release): 1.2.4-rc.6

This commit is contained in:
Théo LAGACHE
2026-01-28 16:39:46 +01:00
parent 4942591d7f
commit 6e8932ff52
8 changed files with 164 additions and 4 deletions
@@ -11,6 +11,7 @@ import {SideBarFooterCredit} from "@/components/wrappers/dashboard/common/sideba
import {OrganizationCombobox} from "@/components/wrappers/dashboard/organization/organization-combobox";
import {env} from "@/env.mjs";
import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button.server";
import {UpdateNotification} from "@/features/updates/components/update-notification";
export function AppSidebar() {
const projectName = env.PROJECT_NAME;
@@ -31,6 +32,8 @@ export function AppSidebar() {
<SidebarMenuCustomMain/>
</SidebarContent>
<UpdateNotification />
<SidebarMenu className="mb-2">
<SidebarMenuItem className="p-2">
<LoggedInButton/>
+2 -1
View File
@@ -44,10 +44,11 @@ export const env = createEnv({
},
client: {
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
NEXT_PUBLIC_UPDATE_CHANNEL: z.enum(["stable", "beta", "rc"]).default("stable"),
},
runtimeEnv: {
NEXT_PUBLIC_PROJECT_VERSION: version || "Unknown Version",
NEXT_PUBLIC_UPDATE_CHANNEL: process.env.NEXT_PUBLIC_UPDATE_CHANNEL || "stable",
PROJECT_NAME: process.env.PROJECT_NAME,
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
@@ -0,0 +1,59 @@
"use client";
import { useUpdateCheck } from "../hooks/use-update-check";
import { useSidebar, SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton } from "@/components/ui/sidebar";
import { X, ArrowUpCircle } from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
export const UpdateNotification = () => {
const { isUpdateAvailable, latestRelease, dismissUpdate } = useUpdateCheck();
const { state } = useSidebar();
if (!isUpdateAvailable || !latestRelease || state !== "expanded") {
return null;
}
return (
<SidebarGroup className="py-0">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem className="px-2">
<div className="relative flex flex-col gap-2 rounded-lg border bg-primary/5 p-3 text-sidebar-foreground border-primary/20">
<button
onClick={dismissUpdate}
className="absolute right-2 top-2 rounded-md p-0.5 text-muted-foreground/50 hover:bg-sidebar-accent hover:text-foreground transition-colors"
>
<X className="size-3" />
<span className="sr-only">Dismiss</span>
</button>
<div className="flex items-center gap-2">
<div className="flex size-6 items-center justify-center rounded-md bg-primary/10 text-primary">
<ArrowUpCircle className="size-4" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-[12px] font-semibold leading-none">Update available</span>
<span className="text-[10px] text-muted-foreground font-medium">
v{latestRelease.tag_name.replace(/^v/, "")}
</span>
</div>
</div>
<SidebarMenuButton
asChild
variant="outline"
size="sm"
className="h-7 w-full justify-center bg-background text-[10px] font-medium shadow-none hover:bg-primary/5 hover:text-primary hover:border-primary/30"
>
<Link href={latestRelease.html_url} target="_blank">
See what's new
</Link>
</SidebarMenuButton>
</div>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
};
@@ -0,0 +1,60 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { getLatestRelease } from "../services/github";
import { env } from "@/env.mjs";
import { useEffect, useState } from "react";
const DISMISS_KEY = "portabase-update-dismissed";
const DISMISS_DURATION = 1000 * 60 * 60 * 24;
export const useUpdateCheck = () => {
const [isDismissed, setIsDismissed] = useState(true);
const { data: latestRelease, isLoading } = useQuery({
queryKey: ["latest-release", env.NEXT_PUBLIC_UPDATE_CHANNEL],
queryFn: () => getLatestRelease(env.NEXT_PUBLIC_UPDATE_CHANNEL as any),
staleTime: 1000 * 60 * 60,
});
const currentVersion = env.NEXT_PUBLIC_PROJECT_VERSION;
useEffect(() => {
if (!latestRelease) return;
const latestVersion = latestRelease.tag_name.replace(/^v/, "");
const cleanCurrentVersion = currentVersion?.replace(/^v/, "");
if (latestVersion && cleanCurrentVersion && latestVersion !== cleanCurrentVersion) {
const dismissedData = localStorage.getItem(DISMISS_KEY);
if (dismissedData) {
const { version, timestamp } = JSON.parse(dismissedData);
const now = Date.now();
if (version === latestRelease.tag_name && now - timestamp < DISMISS_DURATION) {
setIsDismissed(true);
return;
}
}
setIsDismissed(false);
} else {
setIsDismissed(true);
}
}, [latestRelease, currentVersion]);
const dismissUpdate = () => {
if (latestRelease) {
localStorage.setItem(DISMISS_KEY, JSON.stringify({
version: latestRelease.tag_name,
timestamp: Date.now()
}));
setIsDismissed(true);
}
};
return {
latestRelease,
isLoading,
isUpdateAvailable: !isDismissed && latestRelease,
dismissUpdate
};
};
+34
View File
@@ -0,0 +1,34 @@
export interface GitHubRelease {
tag_name: string;
html_url: string;
prerelease: boolean;
name: string;
body: string;
}
export const getLatestRelease = async (channel: "stable" | "beta" | "rc" = "stable"): Promise<GitHubRelease | null> => {
try {
const response = await fetch("https://api.github.com/repos/Portabase/portabase/releases");
if (!response.ok) {
return null;
}
const releases: GitHubRelease[] = await response.json();
if (channel === "stable") {
return releases.find(r => !r.prerelease) || null;
}
if (channel === "beta") {
return releases.find(r => r.tag_name.includes("beta") || !r.prerelease) || null;
}
if (channel === "rc") {
return releases.find(r => r.tag_name.includes("rc") || !r.prerelease) || null;
}
return releases[0] || null;
} catch (error) {
console.error("Failed to fetch latest release", error);
return null;
}
};