-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from dedeard/dev
Create Guestbook page using firebase.
- Loading branch information
Showing
12 changed files
with
1,121 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
src/app/(root)/(app)/guestbook/components/FormSignGuestbook.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
'use client' | ||
|
||
import React, { useState } from 'react' | ||
import { addDoc, collection, serverTimestamp } from 'firebase/firestore' | ||
import { useAuth } from '@/contexts/AuthContext' | ||
import { db } from '@/utils/firebase' | ||
|
||
const FormSignGuestbook = () => { | ||
const { isInitLoading, isAuthLoading, error, login, logout, user } = useAuth() | ||
const [value, setValue] = useState('') | ||
const [formError, setFormError] = useState('') | ||
|
||
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => { | ||
e.preventDefault() | ||
|
||
const message = value | ||
|
||
setFormError('') | ||
if (value.length == 0) return setFormError('Message Is Required.') | ||
|
||
try { | ||
const data = { | ||
userId: user?.uid, | ||
name: user?.displayName, | ||
createdAt: serverTimestamp(), | ||
message, | ||
} | ||
setValue('') | ||
await addDoc(collection(db, 'guestbook'), data) | ||
} catch (e: any) { | ||
setFormError(e.message) | ||
setValue(message) | ||
} | ||
} | ||
|
||
return ( | ||
<> | ||
<p className="mb-3 text-sm font-light md:text-base"> | ||
{isInitLoading && 'Loading...'} | ||
{!isInitLoading && | ||
(!user ? ( | ||
<> | ||
Welcome! Please sign in to leave a message.{' '} | ||
<button | ||
type="button" | ||
className="text-blue-600 dark:text-blue-500" | ||
disabled={isAuthLoading} | ||
onClick={() => login('google.com')} | ||
> | ||
Sign In With Google | ||
</button>{' '} | ||
or{' '} | ||
<button | ||
type="button" | ||
className="text-blue-600 dark:text-blue-500" | ||
disabled={isAuthLoading} | ||
onClick={() => login('github.com')} | ||
> | ||
Sign In With Github | ||
</button> | ||
</> | ||
) : ( | ||
<> | ||
Signed In as <span className="font-semibold">{user?.displayName}</span>!{' '} | ||
<button type="button" className="text-red-600 dark:text-red-500" disabled={isAuthLoading} onClick={() => logout()}> | ||
Sign Out | ||
</button> | ||
</> | ||
))} | ||
</p> | ||
|
||
{(error || formError) && ( | ||
<div className="mb-3 border-l-4 border-red-500 bg-red-500/10 px-3 py-4 font-bold backdrop-blur-lg">{error || formError}</div> | ||
)} | ||
|
||
<form className="mb-3 flex gap-3" onSubmit={handleSubmit}> | ||
<div className="flex-1 backdrop-blur"> | ||
<input | ||
type="text" | ||
name="message" | ||
maxLength={256} | ||
placeholder="Write your message here..." | ||
disabled={!user} | ||
className="block h-14 w-full border-black/10 bg-white text-sm text-black placeholder-black/60 opacity-60 focus:border-black/10 focus:border-b-black focus:opacity-100 focus:ring-0 dark:border-white/10 dark:bg-black dark:text-white dark:placeholder-white/60 dark:focus:border-b-white" | ||
value={value} | ||
onChange={(e) => setValue(e.currentTarget.value)} | ||
/> | ||
</div> | ||
<div className="backdrop-blur"> | ||
<button | ||
type="submit" | ||
className="flex h-14 items-center gap-3 border border-black/10 bg-white px-3 font-bold uppercase opacity-75 dark:border-white/10 dark:bg-black hover:[&:not(:disabled)]:opacity-100" | ||
disabled={!user} | ||
> | ||
Submit | ||
</button> | ||
</div> | ||
</form> | ||
</> | ||
) | ||
} | ||
|
||
export default FormSignGuestbook |
70 changes: 70 additions & 0 deletions
70
src/app/(root)/(app)/guestbook/components/GuestbookMessages.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
'use client' | ||
|
||
import React, { useState, useEffect } from 'react' | ||
import { Timestamp, collection, limit, onSnapshot, orderBy, query } from 'firebase/firestore' | ||
import { db } from '@/utils/firebase' | ||
import { IGuestbookMessage } from '@/types' | ||
|
||
function formatDate(date: Date) { | ||
const formatter = new Intl.DateTimeFormat('en-US', { | ||
year: 'numeric', | ||
month: '2-digit', | ||
day: '2-digit', | ||
hour: '2-digit', | ||
minute: '2-digit', | ||
hour12: true, | ||
}) | ||
|
||
return formatter.format(date).replace(/\//g, '-') | ||
} | ||
|
||
const GuestbookMessages: React.FC<{ initialMessages: string }> = ({ initialMessages }) => { | ||
const [messages, setMessages] = useState<IGuestbookMessage[]>(() => { | ||
return JSON.parse(initialMessages).map((el: IGuestbookMessage) => ({ | ||
...el, | ||
createdAt: el.createdAt && new Timestamp(el.createdAt.seconds, el.createdAt.nanoseconds), | ||
})) | ||
}) | ||
|
||
useEffect(() => { | ||
const colRef = collection(db, 'guestbook') | ||
const q = query(colRef, orderBy('createdAt', 'desc'), limit(100)) | ||
|
||
const unsub = onSnapshot(q, (querySnapshot) => { | ||
const messages: IGuestbookMessage[] = [] | ||
querySnapshot.forEach((doc) => { | ||
messages.push({ _id: doc.id, ...doc.data() } as IGuestbookMessage) | ||
}) | ||
setMessages(messages) | ||
}) | ||
|
||
return () => unsub() | ||
}, []) | ||
|
||
return ( | ||
<div className="border border-black/5 bg-white/30 backdrop-blur dark:border-white/5 dark:bg-black/30"> | ||
<div className="divide-y"> | ||
{messages.map((message) => ( | ||
<p | ||
key={message._id} | ||
className="flex flex-col items-start gap-x-3 gap-y-1 border-black/5 p-3 text-xs dark:border-white/5 md:!text-sm lg:flex-row lg:py-2" | ||
> | ||
<span className="flex w-full shrink-0 items-center justify-between gap-x-2 truncate opacity-75 lg:w-36"> | ||
{message.name.substring(0, 20)} | ||
<span className="flex shrink-0 items-center justify-center gap-x-2 text-xs opacity-75 lg:hidden"> | ||
{formatDate(message.createdAt?.toDate() || new Date())} | ||
</span> | ||
</span> | ||
<span className="hidden lg:block">:</span> | ||
<span className="flex-1 whitespace-pre-line">{message.message}</span> | ||
<span className="hidden shrink-0 items-center justify-center gap-x-2 text-xs opacity-75 lg:flex"> | ||
{formatDate(message.createdAt?.toDate() || new Date())} | ||
</span> | ||
</p> | ||
))} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
export default GuestbookMessages |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import type { Metadata } from 'next' | ||
import PageTitle from '../components/PageTitle' | ||
import FormSignGuestbook from './components/FormSignGuestbook' | ||
import GuestbookMessages from './components/GuestbookMessages' | ||
import { collection, limit, getDocs, orderBy, query } from 'firebase/firestore' | ||
import { db } from '@/utils/firebase' | ||
import { IGuestbookMessage } from '@/types' | ||
|
||
export const dynamic = 'force-dynamic' | ||
|
||
export const metadata: Metadata = { | ||
title: 'Guestbook - Dede Ariansya', | ||
openGraph: { | ||
title: 'Guestbook - Dede Ariansya', | ||
url: '/guestbook', | ||
}, | ||
alternates: { | ||
canonical: '/guestbook', | ||
}, | ||
} | ||
|
||
export default async function GuestbookPage() { | ||
const messages = await loadMessages() | ||
|
||
return ( | ||
<> | ||
<PageTitle title="G-book" /> | ||
<FormSignGuestbook /> | ||
<GuestbookMessages initialMessages={JSON.stringify(messages)} /> | ||
</> | ||
) | ||
} | ||
|
||
const loadMessages = async () => { | ||
const colRef = collection(db, 'guestbook') | ||
const q = query(colRef, orderBy('createdAt', 'desc'), limit(100)) | ||
|
||
const querySnapshot = await getDocs(q) | ||
|
||
const messages: IGuestbookMessage[] = [] | ||
querySnapshot.forEach((doc) => { | ||
messages.push({ _id: doc.id, ...doc.data() } as IGuestbookMessage) | ||
}) | ||
|
||
return messages | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
const FIREBASE_ERRORS: Record<string, string> = { | ||
'auth/operation-not-allowed': 'This operation is not allowed.', | ||
'auth/user-disabled': 'This user account has been disabled.', | ||
'auth/user-not-found': 'No user found with this email address.', | ||
'auth/wrong-password': 'The password is incorrect for the provided email.', | ||
'auth/email-already-in-use': 'The email address is already in use by another account.', | ||
'auth/invalid-email': 'The provided value for the email is invalid.', | ||
'auth/weak-password': 'The password must be at least six characters long.', | ||
'auth/account-exists-with-different-credential': | ||
'An account already exists with the same email address but different sign-in credentials.', | ||
'auth/network-request-failed': 'Network request failed. Please try again.', | ||
'auth/too-many-requests': 'We have blocked all requests from this device due to unusual activity. Try again later.', | ||
'auth/requires-recent-login': 'This operation requires recent authentication. Log in again before retrying this request.', | ||
'auth/credential-already-in-use': 'This credential is already associated with a different user account.', | ||
'auth/expired-action-code': 'The action code has expired. Please request a new one.', | ||
'auth/invalid-action-code': 'The action code is invalid. This can happen if the code is malformed or has already been used.', | ||
'auth/user-token-expired': "User's token has been expired, you need to sign In again.", | ||
'auth/popup-closed-by-user': 'The sign-in popup was closed by the user.', | ||
|
||
'auth/invalid-login-credentials': 'The provided login credentials are invalid. Please check your email and password.', | ||
'auth/invalid-credential': 'The supplied auth credential is malformed or has expired.', | ||
'auth/invalid-user-token': "The user's credential is no longer valid. The user must sign in again.", | ||
'auth/null-user': 'The user is null. Please authenticate again.', | ||
'auth/app-deleted': 'The instance of FirebaseApp was deleted.', | ||
'auth/unauthorized-domain': 'The domain of the current site is not authorized for OAuth operations.', | ||
'auth/user-mismatch': 'The supplied credentials do not correspond to the previously signed in user.', | ||
'auth/invalid-provider-id': 'The supplied provider ID is not supported for this operation.', | ||
'auth/invalid-verification-code': 'The SMS verification code used to create the phone auth credential is invalid.', | ||
'auth/invalid-verification-id': 'The verification ID used to create the phone auth credential is invalid.', | ||
} | ||
|
||
export default FIREBASE_ERRORS |
Oops, something went wrong.