Skip to content

Commit

Permalink
Merge pull request #2 from dedeard/dev
Browse files Browse the repository at this point in the history
Create Guestbook page using firebase.
  • Loading branch information
dedeard authored Feb 25, 2024
2 parents 24a3eb6 + 1831770 commit dd7bb77
Show file tree
Hide file tree
Showing 12 changed files with 1,121 additions and 137 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@react-spring/web": "^9.7.3",
"@vercel/analytics": "^1.1.1",
"@vercel/speed-insights": "^1.0.1",
"firebase": "^10.8.0",
"next": "^14.0.2",
"next-mdx-remote": "^4.4.1",
"react": "^18.2.0",
Expand Down
6 changes: 3 additions & 3 deletions src/app/(root)/(app)/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ export default function ContactPage() {
<div className="flex flex-col lg:flex-row">
<div className="mb-4 w-full text-center lg:w-[480px] lg:text-left">
<div className="mb-8">
<h2 className="mb-3 text-lg font-bold">STREET ADDRESS</h2>
<h2 className="mb-3 text-lg font-bold">TEMPORARY ADDRESS</h2>
<p className="text-sm leading-5">
Royal Sentraland BTP Cluster Sunderland E05/01. <br />
Maros, Sulawesi Selatan.
Bali, Indonesia. <br />
80361
</p>
</div>
<div className="mb-8">
Expand Down
103 changes: 103 additions & 0 deletions src/app/(root)/(app)/guestbook/components/FormSignGuestbook.tsx
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 src/app/(root)/(app)/guestbook/components/GuestbookMessages.tsx
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
46 changes: 46 additions & 0 deletions src/app/(root)/(app)/guestbook/page.tsx
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
}
25 changes: 14 additions & 11 deletions src/app/Providers.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { AuthProvider } from '@/contexts/AuthContext'
import { CursorFollowerProvider } from '@/contexts/CursorFollowerContext'
import { MountProvider } from '@/contexts/MountContext'
import { NavigationProvider } from '@/contexts/NavigationContext'
Expand All @@ -9,17 +10,19 @@ import { ThemeProvider } from '@/contexts/ThemeContext'

const Providers: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<ProgressBarProvider>
<MountProvider>
<NavigationProvider>
<CursorFollowerProvider>
<ThemeProvider defaultDark>
<RootBackgroundProvider>{children}</RootBackgroundProvider>
</ThemeProvider>
</CursorFollowerProvider>
</NavigationProvider>
</MountProvider>
</ProgressBarProvider>
<AuthProvider>
<ProgressBarProvider>
<MountProvider>
<NavigationProvider>
<CursorFollowerProvider>
<ThemeProvider defaultDark>
<RootBackgroundProvider>{children}</RootBackgroundProvider>
</ThemeProvider>
</CursorFollowerProvider>
</NavigationProvider>
</MountProvider>
</ProgressBarProvider>
</AuthProvider>
)
}

Expand Down
15 changes: 14 additions & 1 deletion src/constans/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FiHome, FiGlobe, FiClipboard, FiMail, FiUser } from 'react-icons/fi'
import { FiHome, FiGlobe, FiClipboard, FiMail, FiUser, FiBookOpen } from 'react-icons/fi'

export const HOST = process.env.NEXT_PUBLIC_HOST || 'http://localhost:3000'

Expand All @@ -8,6 +8,7 @@ export const NAV_ITEMS = [
{ path: '/blog', label: 'Blog', Icon: FiGlobe },
{ path: '/projects', label: 'Projects', Icon: FiClipboard },
{ path: '/contact', label: 'Contact', Icon: FiMail },
{ path: '/guestbook', label: 'Guestbook', Icon: FiBookOpen },
]

export const PAGE_TITLES = {
Expand All @@ -16,6 +17,7 @@ export const PAGE_TITLES = {
'/blog': 'Blog',
'/projects': 'Projects',
'/contact': 'Contact',
'/guestbook': 'G-book',
}

export const SOCIALS = {
Expand All @@ -28,3 +30,14 @@ export const SOCIALS = {
export const RESUME_URL = 'https://drive.google.com/file/d/1ytO7InWLVjJGryqRC0QZdc60bU1iesph/view?usp=sharing'

export const FORMSPREE_KEY = 'xoqyaqqe'

export const FIREBASE_CONFIG = {
apiKey: 'AIzaSyCVXW6MTdRVtYPTOoV92ruBQ3ZQcF5Ho0g',
authDomain: 'dede-ard.firebaseapp.com',
databaseURL: 'https://dede-ard.firebaseio.com',
projectId: 'dede-ard',
storageBucket: 'dede-ard.appspot.com',
messagingSenderId: '120930847292',
appId: '1:120930847292:web:eb77034f59e9ee37b65139',
measurementId: 'G-KJRFL3X06T',
}
32 changes: 32 additions & 0 deletions src/constans/firebase-errors.ts
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
Loading

0 comments on commit dd7bb77

Please sign in to comment.