feat: add updateActiveProject function for in-place project updates

This commit is contained in:
Dries Augustyns
2026-04-24 14:58:06 +02:00
parent 98f1e80fca
commit 6f31fa2503
3 changed files with 57 additions and 45 deletions
+28 -24
View File
@@ -107,17 +107,19 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
<CardTitle>Bounce Rate</CardTitle> <CardTitle>Bounce Rate</CardTitle>
<CardDescription>Hard bounces indicate invalid or non-existent email addresses</CardDescription> <CardDescription>Hard bounces indicate invalid or non-existent email addresses</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="p-0">
<HealthMetric <div className="divide-y divide-neutral-100">
label="Last 7 Days" <HealthMetric
level={levels.bounce7Day} label="Last 7 Days"
detail={`${status.sevenDay.bounceRate.toFixed(2)}% bounce rate (${status.sevenDay.bounces.toLocaleString()} of ${status.sevenDay.total.toLocaleString()} emails)`} level={levels.bounce7Day}
/> detail={`${status.sevenDay.bounceRate.toFixed(2)}% bounce rate (${status.sevenDay.bounces.toLocaleString()} of ${status.sevenDay.total.toLocaleString()} emails)`}
<HealthMetric />
label="All Time" <HealthMetric
level={levels.bounceAllTime} label="All Time"
detail={`${status.allTime.bounceRate.toFixed(2)}% bounce rate (${status.allTime.bounces.toLocaleString()} of ${status.allTime.total.toLocaleString()} emails)`} level={levels.bounceAllTime}
/> detail={`${status.allTime.bounceRate.toFixed(2)}% bounce rate (${status.allTime.bounces.toLocaleString()} of ${status.allTime.total.toLocaleString()} emails)`}
/>
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -127,17 +129,19 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
<CardTitle>Complaint Rate</CardTitle> <CardTitle>Complaint Rate</CardTitle>
<CardDescription>Complaints occur when recipients mark emails as spam</CardDescription> <CardDescription>Complaints occur when recipients mark emails as spam</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="p-0">
<HealthMetric <div className="divide-y divide-neutral-100">
label="Last 7 Days" <HealthMetric
level={levels.complaint7Day} label="Last 7 Days"
detail={`${status.sevenDay.complaintRate.toFixed(3)}% complaint rate (${status.sevenDay.complaints.toLocaleString()} of ${status.sevenDay.total.toLocaleString()} emails)`} level={levels.complaint7Day}
/> detail={`${status.sevenDay.complaintRate.toFixed(3)}% complaint rate (${status.sevenDay.complaints.toLocaleString()} of ${status.sevenDay.total.toLocaleString()} emails)`}
<HealthMetric />
label="All Time" <HealthMetric
level={levels.complaintAllTime} label="All Time"
detail={`${status.allTime.complaintRate.toFixed(3)}% complaint rate (${status.allTime.complaints.toLocaleString()} of ${status.allTime.total.toLocaleString()} emails)`} level={levels.complaintAllTime}
/> detail={`${status.allTime.complaintRate.toFixed(3)}% complaint rate (${status.allTime.complaints.toLocaleString()} of ${status.allTime.total.toLocaleString()} emails)`}
/>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -155,10 +159,10 @@ function HealthMetric({label, level, detail}: HealthMetricProps) {
const Icon = config.icon; const Icon = config.icon;
return ( return (
<div className="flex items-center justify-between border border-neutral-200 rounded-lg p-4"> <div className="flex items-center justify-between px-6 py-4">
<div> <div>
<h3 className="font-medium text-neutral-900">{label}</h3> <h3 className="font-medium text-neutral-900">{label}</h3>
<p className="text-sm text-neutral-600 mt-1">{detail}</p> <p className="text-sm text-neutral-600 mt-0.5">{detail}</p>
</div> </div>
<div className={`flex items-center gap-2 ${config.color}`}> <div className={`flex items-center gap-2 ${config.color}`}>
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
@@ -7,6 +7,7 @@ import {useProjects} from '../hooks/useProject';
interface ActiveProjectContextValue { interface ActiveProjectContextValue {
activeProject: Project | null; activeProject: Project | null;
setActiveProject: (project: Project) => void; setActiveProject: (project: Project) => void;
updateActiveProject: (project: Project) => void;
availableProjects: Project[]; availableProjects: Project[];
isLoading: boolean; isLoading: boolean;
} }
@@ -55,9 +56,16 @@ export function ActiveProjectProvider({children}: {children: ReactNode}) {
void mutate(() => true, undefined, {revalidate: true}); void mutate(() => true, undefined, {revalidate: true});
}; };
// Updates the active project's data in-place without invalidating the SWR cache.
// Use this when saving settings for the current project, not when switching projects.
const updateActiveProject = (project: Project) => {
setActiveProjectState(project);
};
const value: ActiveProjectContextValue = { const value: ActiveProjectContextValue = {
activeProject, activeProject,
setActiveProject, setActiveProject,
updateActiveProject,
availableProjects: projects ?? [], availableProjects: projects ?? [],
isLoading, isLoading,
}; };
+21 -21
View File
@@ -86,7 +86,7 @@ const buildTabs = (options: {billingEnabled: boolean; smtpEnabled: boolean}): Ta
export default function Settings() { export default function Settings() {
const router = useRouter(); const router = useRouter();
const {activeProject, setActiveProject} = useActiveProject(); const {activeProject, setActiveProject, updateActiveProject} = useActiveProject();
const {mutate: projectsMutate} = useProjects(); const {mutate: projectsMutate} = useProjects();
const {data: config} = useConfig(); const {data: config} = useConfig();
const {data: user} = useUser(); const {data: user} = useUser();
@@ -201,8 +201,8 @@ export default function Settings() {
values, values,
); );
// Update the active project in context // Update the active project in context without invalidating the full SWR cache
setActiveProject(updatedProject); updateActiveProject(updatedProject);
// Refresh projects list // Refresh projects list
await projectsMutate(); await projectsMutate();
@@ -395,6 +395,7 @@ export default function Settings() {
{/* General Tab */} {/* General Tab */}
<TabsContent value="general"> <TabsContent value="general">
<div className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Project Settings</CardTitle> <CardTitle>Project Settings</CardTitle>
@@ -491,12 +492,6 @@ export default function Settings() {
)} )}
/> />
<div className="flex justify-end">
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</div>
{/* Success/Error Messages */} {/* Success/Error Messages */}
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{successMessage && ( {successMessage && (
@@ -520,13 +515,19 @@ export default function Settings() {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
<div className="flex justify-end">
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</form> </form>
</Form> </Form>
</CardContent> </CardContent>
</Card> </Card>
{/* API Keys - Separate Card */} {/* API Keys - Separate Card */}
<Card className="mt-6"> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -554,7 +555,7 @@ export default function Settings() {
</Card> </Card>
{/* Danger Zone - Separate Card */} {/* Danger Zone - Separate Card */}
<Card className="border-red-200 mt-6"> <Card className="border-red-200">
<CardHeader className="border-b border-red-100 bg-red-50"> <CardHeader className="border-b border-red-100 bg-red-50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-lg shadow-sm border border-red-200"> <div className="p-2 bg-white rounded-lg shadow-sm border border-red-200">
@@ -568,12 +569,12 @@ export default function Settings() {
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pt-6 space-y-6"> <CardContent className="p-0">
{/* Reset Project */} <div className="divide-y divide-neutral-100">
<div className="group"> {/* Reset Project */}
<div className="flex items-start justify-between gap-4 p-5 rounded-lg border border-neutral-200 bg-white transition-all"> <div className="flex items-start justify-between gap-4 px-6 py-5">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-1.5">
<Database className="h-4 w-4 text-amber-700" /> <Database className="h-4 w-4 text-amber-700" />
<h4 className="font-semibold text-neutral-900">Reset Project Data</h4> <h4 className="font-semibold text-neutral-900">Reset Project Data</h4>
</div> </div>
@@ -590,13 +591,11 @@ export default function Settings() {
Reset Data Reset Data
</Button> </Button>
</div> </div>
</div>
{/* Delete Project */} {/* Delete Project */}
<div className="group"> <div className="flex items-start justify-between gap-4 px-6 py-5 bg-red-50/40">
<div className="flex items-start justify-between gap-4 p-5 rounded-lg border-2 border-red-200 bg-red-50/50 transition-all">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-1.5">
<AlertTriangle className="h-4 w-4 text-red-600" /> <AlertTriangle className="h-4 w-4 text-red-600" />
<h4 className="font-semibold text-red-900">Delete Project Permanently</h4> <h4 className="font-semibold text-red-900">Delete Project Permanently</h4>
</div> </div>
@@ -624,6 +623,7 @@ export default function Settings() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div>
</TabsContent> </TabsContent>
{/* Billing Tab */} {/* Billing Tab */}