diff --git a/app/account/layout.tsx b/app/account/layout.tsx
index 17d8dee..300f896 100644
--- a/app/account/layout.tsx
+++ b/app/account/layout.tsx
@@ -1,4 +1,5 @@
-import React from "react";
+import React from "react"
+import { Toaster } from "@/components/ui/sonner"
export default function AccountLayout({
children,
@@ -6,7 +7,12 @@ export default function AccountLayout({
children: React.ReactNode
}) {
return (
-
{children}
+ <>
+
+ {children}
+
+
+ >
)
}
diff --git a/app/globals.css b/app/globals.css
index 3bcb11f..659882f 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -20,6 +20,10 @@
--color-accent-foreground: oklch(var(--accent-foreground));
--color-destructive: oklch(var(--destructive));
--color-destructive-foreground: oklch(var(--destructive-foreground));
+ --color-error: oklch(var(--error));
+ --color-error-foreground: oklch(var(--error-foreground));
+ --color-success: oklch(var(--success));
+ --color-success-foreground: oklch(var(--success-foreground));
--color-border: oklch(var(--border));
--color-input: oklch(var(--input));
--color-ring: oklch(var(--ring));
@@ -105,6 +109,10 @@
--accent-foreground: 0.208 0.042 265.755;
--destructive: 0.577 0.245 27.325;
--destructive-foreground: 0.984 0.003 247.858;
+ --error: 0.577 0.245 27.325;
+ --error-foreground: 0.984 0.003 247.858;
+ --success: 0.6 0.118 184.704;
+ --success-foreground: 0.984 0.003 247.858;
--border: 0.929 0.013 255.508;
--input: 0.929 0.013 255.508;
--ring: 0.704 0.04 256.788;
@@ -145,6 +153,10 @@
--accent-foreground: 0.984 0.003 247.858;
--destructive: 0.704 0.191 22.216;
--destructive-foreground: 0.984 0.003 247.858;
+ --error: 0.704 0.191 22.216;
+ --error-foreground: 0.984 0.003 247.858;
+ --success: 0.696 0.17 162.48;
+ --success-foreground: 0.984 0.003 247.858;
--border: 1 0 0 / 10%;
--input: 1 0 0 / 15%;
--ring: 0.551 0.027 264.364;
diff --git a/components/cards/dashboard/Settings/ChangeAuthentikPassword.tsx b/components/cards/dashboard/Settings/ChangeAuthentikPassword.tsx
index a5da567..59c2fe7 100644
--- a/components/cards/dashboard/Settings/ChangeAuthentikPassword.tsx
+++ b/components/cards/dashboard/Settings/ChangeAuthentikPassword.tsx
@@ -1,10 +1,10 @@
"use client"
-import React, { useState } from "react"
+import React, { useState, useRef, useEffect } from "react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
-import { Key, Loader2 } from "lucide-react"
+import { Key, Loader2, CheckCircleIcon, XCircleIcon } from "lucide-react"
import Link from "next/link"
import {
Dialog,
@@ -15,17 +15,22 @@ import {
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog"
+import { toast } from "sonner"
+import { motion, useAnimationControls } from "framer-motion"
export function ChangeAuthentikPassword() {
const [newPassword, setNewPassword] = useState("")
const [loading, setLoading] = useState(false)
- const [message, setMessage] = useState(null)
const [open, setOpen] = useState(false)
+ const holdTimeoutRef = useRef(null)
+ const intervalRef = useRef(null)
+ const controls = useAnimationControls()
+ const [isHolding, setIsHolding] = useState(false)
+ const holdDuration = 10
+ const [remainingTime, setRemainingTime] = useState(holdDuration)
- const handlePasswordChange = async (e: React.FormEvent) => {
- e.preventDefault()
+ const submitPasswordChange = async () => {
setLoading(true)
- setMessage(null)
try {
const response = await fetch("/api/auth/password", {
method: "POST",
@@ -37,32 +42,143 @@ export function ChangeAuthentikPassword() {
const resData = await response.json()
if (response.ok && resData.success) {
- setMessage("Password Updated")
- setLoading(false)
- // Close dialog after change
+ toast("Password updated successfully!", {
+ icon: ,
+ style: {
+ backgroundColor: "oklch(var(--success))",
+ color: "oklch(var(--success-foreground))",
+ },
+ })
setTimeout(() => {
setOpen(false)
setNewPassword("")
+ controls.set({ "--progress": "0%" })
}, 1500)
- // TODO: Show a toast that password was changed
} else if (resData.error) {
- setMessage(resData.error)
- setLoading(false)
+ toast("An error occurred", {
+ description: resData.error,
+ icon: ,
+ style: {
+ backgroundColor: "oklch(var(--error))",
+ color: "oklch(var(--error-foreground))",
+ },
+ })
+ controls.set({ "--progress": "0%" })
} else {
- setMessage("[1] Failed to Update")
- setLoading(false)
+ toast("Failed to Update", {
+ description: "An unknown error occurred [1]",
+ icon: ,
+ style: {
+ backgroundColor: "oklch(var(--error))",
+ color: "oklch(var(--error-foreground))",
+ },
+ })
+ controls.set({ "--progress": "0%" })
}
} catch (error) {
console.log(error)
- setMessage("[2] Failed to Update")
+ toast("Failed to Update", {
+ description: "An unknown error occurred [2]",
+ icon: ,
+ style: {
+ backgroundColor: "oklch(var(--error))",
+ color: "oklch(var(--error-foreground))",
+ },
+ })
+ controls.set({ "--progress": "0%" })
+ } finally {
setLoading(false)
+ setIsHolding(false)
+ if (holdTimeoutRef.current) {
+ clearTimeout(holdTimeoutRef.current)
+ holdTimeoutRef.current = null
+ }
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ intervalRef.current = null
+ }
}
- };
+ }
+
+ const handleFormSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ }
+
+ const holdDurationMs = holdDuration * 1000
+
+ const handleHoldStart = () => {
+ if (loading || newPassword.length < 8) return
+
+ setIsHolding(true)
+ controls.set({ "--progress": "0%" })
+ controls.start(
+ { "--progress": "100%" },
+ { duration: holdDuration, ease: "linear" }
+ )
+
+ holdTimeoutRef.current = setTimeout(() => {
+ console.log("[i] Hold complete, submitting...")
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ intervalRef.current = null
+ }
+ submitPasswordChange()
+ holdTimeoutRef.current = null
+ }, holdDurationMs)
+
+ setRemainingTime(holdDuration)
+ if (intervalRef.current) clearInterval(intervalRef.current)
+ intervalRef.current = setInterval(() => {
+ setRemainingTime((prevTime) => {
+ if (prevTime <= 1) {
+ if (intervalRef.current) clearInterval(intervalRef.current)
+ return 0
+ }
+ return prevTime - 1
+ })
+ }, 1000)
+ }
+
+ const handleHoldEnd = () => {
+ if (holdTimeoutRef.current) {
+ clearTimeout(holdTimeoutRef.current)
+ holdTimeoutRef.current = null
+ }
+ if (isHolding) {
+ console.log("[i] Hold interrupted")
+ controls.stop()
+ controls.set({ "--progress": "0%" })
+ setIsHolding(false)
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ intervalRef.current = null
+ }
+ }
+ }
+
+ useEffect(() => {
+ return () => {
+ if (holdTimeoutRef.current) {
+ clearTimeout(holdTimeoutRef.current)
+ }
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ }
+ }
+ }, [])
+
+ useEffect(() => {
+ if (!open) {
+ handleHoldEnd()
+ setLoading(false)
+ setNewPassword("")
+ }
+ }, [open])
return (