-
-
+
);
diff --git a/src/auth/action.ts b/src/auth/action.ts
new file mode 100644
index 00000000..734da6e9
--- /dev/null
+++ b/src/auth/action.ts
@@ -0,0 +1,21 @@
+'use server';
+
+import { CREDENTIALS_SIGN_IN_ERROR, signIn, signOut } from '@/auth';
+
+export const signInAction = async (
+ _prevState: string | undefined,
+ formData: FormData,
+) => {
+ try {
+ await signIn('credentials', Object.fromEntries(formData));
+ } catch (error) {
+ if ((error as Error).message.includes(CREDENTIALS_SIGN_IN_ERROR)) {
+ return CREDENTIALS_SIGN_IN_ERROR;
+ }
+ throw error;
+ }
+};
+
+export const signOutAction = async () => {
+ await signOut();
+};
diff --git a/src/auth/index.ts b/src/auth/index.ts
index f548eb5c..db3cc98d 100644
--- a/src/auth/index.ts
+++ b/src/auth/index.ts
@@ -1,25 +1,17 @@
import { isPathProtected } from '@/site/paths';
-import NextAuth, { User, type DefaultSession } from 'next-auth';
+import NextAuth, { User } from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
-declare module 'next-auth' {
- interface Session {
- user: {
- id: string
- } & DefaultSession['user']
- }
-}
+export const CREDENTIALS_SIGN_IN_ERROR = 'CredentialsSignin';
export const {
handlers: { GET, POST },
+ signIn,
+ signOut,
auth,
} = NextAuth({
providers: [
Credentials({
- credentials: {
- email: { label: 'Email', type: 'text' },
- password: { label: 'Password', type: 'password' },
- },
async authorize({ email, password }) {
if (
process.env.ADMIN_EMAIL && process.env.ADMIN_EMAIL === email &&
diff --git a/src/cache/index.ts b/src/cache/index.ts
index 282baf77..ccc85309 100644
--- a/src/cache/index.ts
+++ b/src/cache/index.ts
@@ -15,7 +15,7 @@ import {
} from '@/services/postgres';
import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo';
import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob';
-import { AuthSession } from 'next-auth';
+import type { Session } from 'next-auth';
import { Camera, createCameraKey } from '@/camera';
import { PATHS_ADMIN, PATHS_TO_CACHE } from '@/site/paths';
@@ -226,7 +226,7 @@ export const getBlobPhotoUrlsCached: typeof getBlobPhotoUrls = (...args) =>
}
)();
-export const getImageCacheHeadersForAuth = (session: AuthSession | null) => {
+export const getImageCacheHeadersForAuth = (session: Session | null) => {
return {
'Cache-Control': !session?.user
? 's-maxage=3600, stale-while-revalidate=59'
diff --git a/src/components/ErrorNote.tsx b/src/components/ErrorNote.tsx
new file mode 100644
index 00000000..aaca7190
--- /dev/null
+++ b/src/components/ErrorNote.tsx
@@ -0,0 +1,25 @@
+import { cc } from '@/utility/css';
+import { BiErrorAlt } from 'react-icons/bi';
+
+export default function ErrorNote({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/src/components/FieldSetWithStatus.tsx b/src/components/FieldSetWithStatus.tsx
index c518e37d..bdc62717 100644
--- a/src/components/FieldSetWithStatus.tsx
+++ b/src/components/FieldSetWithStatus.tsx
@@ -1,7 +1,8 @@
'use client';
import { LegacyRef } from 'react';
-import { experimental_useFormStatus as useFormStatus } from 'react-dom';
+// @ts-ignore
+import { useFormStatus } from 'react-dom';
import Spinner from './Spinner';
import { cc } from '@/utility/css';
diff --git a/src/components/ImageInput.tsx b/src/components/ImageInput.tsx
index 3c9d8ca9..3241786b 100644
--- a/src/components/ImageInput.tsx
+++ b/src/components/ImageInput.tsx
@@ -4,9 +4,9 @@ import { blobToImage } from '@/utility/blob';
import { useRef, useState } from 'react';
import { CopyExif } from '@/lib/CopyExif';
import { cc } from '@/utility/css';
-import { AiOutlineCloudUpload } from 'react-icons/ai';
import Spinner from './Spinner';
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';
+import { FiUploadCloud } from 'react-icons/fi';
const INPUT_ID = 'file';
@@ -49,10 +49,10 @@ export default function ImageInput({
>
{loading
- ?
- :
+ : }
Upload Photo
diff --git a/src/components/SubmitButtonWithStatus.tsx b/src/components/SubmitButtonWithStatus.tsx
index e14c2aca..bb9d661e 100644
--- a/src/components/SubmitButtonWithStatus.tsx
+++ b/src/components/SubmitButtonWithStatus.tsx
@@ -1,17 +1,20 @@
'use client';
import { HTMLProps } from 'react';
-import { experimental_useFormStatus as useFormStatus } from 'react-dom';
+// @ts-ignore
+import { useFormStatus } from 'react-dom';
import Spinner from './Spinner';
import { cc } from '@/utility/css';
interface Props extends HTMLProps
{
icon?: JSX.Element
+ styleAsLink?: boolean
}
export default function SubmitButtonWithStatus(props: Props) {
const {
icon,
+ styleAsLink,
children,
disabled,
className,
@@ -28,14 +31,17 @@ export default function SubmitButtonWithStatus(props: Props) {
className={cc(
className,
'inline-flex items-center gap-2',
+ styleAsLink && 'link',
)}
{...buttonProps}
>
{(icon || pending) &&
{pending
?
diff --git a/src/site/FooterAuth.tsx b/src/site/FooterAuth.tsx
index cab90f4e..5a6fc589 100644
--- a/src/site/FooterAuth.tsx
+++ b/src/site/FooterAuth.tsx
@@ -2,22 +2,23 @@
import { cc } from '@/utility/css';
import Link from 'next/link';
-import { useSession, signOut } from 'next-auth/react';
+import { useSession } from 'next-auth/react';
import ThemeSwitcher from '@/site/ThemeSwitcher';
import SiteGrid from '../components/SiteGrid';
import { usePathname } from 'next/navigation';
import { isPathSignIn } from '@/site/paths';
+import { signOutAction } from '@/auth/action';
+import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
const LINK_STYLE = cc(
'cursor-pointer',
- 'hover:text-gray-600',
+ 'hover:text-gray-300',
+ 'hover:dark:text-gray-600',
);
export default function FooterAuth() {
const { data: session, status } = useSession();
- const hasState = status !== 'loading';
-
const path = usePathname();
return (
@@ -27,27 +28,30 @@ export default function FooterAuth() {
'my-8',
'text-dim',
)}>
-
- {hasState
- ? <>
- {session?.user === undefined &&
- <>Loading ...>}
- {session?.user.email && <>
-
{session.user.email}
-
signOut()}
+
+ {status === 'loading'
+ ? <>Loading ...>
+ : <>
+ {session?.user?.email &&
+ {session.user.email}
+
}
+ {status === 'authenticated' &&
+
}
+ {status === 'unauthenticated' &&
+
- Sign Out
-
- >}
- >
- :
- Sign In
- }
+ Sign In
+ }
+ >}
{!isPathSignIn(path) &&
}
}
diff --git a/src/site/globals.css b/src/site/globals.css
index 242dca91..7a080dc2 100644
--- a/src/site/globals.css
+++ b/src/site/globals.css
@@ -72,10 +72,11 @@
px-4
text-base
shadow-sm
- disabled:bg-gray-100 dark:disabled:bg-gray-900 disabled:cursor-not-allowed
active:bg-gray-100 dark:active:bg-gray-900
hover:border-gray-300 dark:hover:border-gray-600
- hover:disabled:border-gray-200
+ disabled:cursor-not-allowed
+ disabled:bg-gray-100 dark:disabled:bg-gray-900
+ disabled:border-gray-200 disabled:dark:border-gray-700
}
button.subtle, .button.subtle {
@apply
@@ -97,6 +98,11 @@
@apply
text-medium
}
+ button.link {
+ @apply
+ p-0 min-h-0
+ border-none active:bg-transparent shadow-none
+ }
/* Toasts */
.toaster [data-sonner-toast] {
@apply