google sign in page done

This commit is contained in:
2026-02-19 11:15:25 +05:30
parent ad6ae63d33
commit 555c03a91c
16 changed files with 1745 additions and 104 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
# Firebase copy to .env and fill with your Firebase project config
# Get these from Firebase Console → Project settings → General → Your apps
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_DATABASE_URL=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_APP_ID=

2
.gitignore vendored
View File

@@ -11,6 +11,8 @@ node_modules
dist
dist-ssr
*.local
.env
.env.local
# Editor directories and files
.vscode/*

View File

@@ -0,0 +1,63 @@
# Auth options for Vite + React (SPA)
In **Next.js**, **NextAuth.js** (now **Auth.js**) is the standard: it gives you OAuth (Google, GitHub, etc.), sessions, callbacks, and JWT/cookies on the server.
For a **Vite + React** app (client-side SPA), there is no single “NextAuth for Vite.” You pick by how much you want to host yourself vs use a service.
---
## 1. **Firebase Authentication** (what this project uses)
- **What it is:** Googles auth service; add SDK, configure providers (e.g. Google), get sign-in in the browser.
- **Pros:** Free tier, no backend required for basic “Sign in with Google,” works great with Vite + React.
- **Cons:** Youre tied to Firebase; for custom session logic you still need your own backend.
- **Use when:** You want the fastest “Sign in with Google” (or email link, etc.) with no backend.
---
## 2. **Clerk**
- **What it is:** Hosted auth (like Auth0) with pre-built React components and APIs.
- **Pros:** Drop-in `<SignIn />` / `<SignUp />`, many providers, user management dashboard, works with any frontend (including Vite + React).
- **Cons:** Paid after free tier; vendor lock-in.
- **Use when:** You want a “NextAuth-like” experience (components + dashboard) without running your own auth server.
---
## 3. **Auth0** (Okta)
- **What it is:** Hosted identity platform; SDK + hosted login pages or embedded UI.
- **Pros:** Enterprise-ready, many providers, fine-grained controls.
- **Cons:** Can be heavy and costly for small apps.
- **Use when:** You need enterprise features or already use Okta.
---
## 4. **Supabase Auth**
- **What it is:** Auth layer from Supabase (DB + realtime + storage).
- **Pros:** Google, magic link, email/password; integrates with Supabase DB and RLS.
- **Cons:** Makes most sense if you use Supabase for data.
- **Use when:** Your backend (or “BaaS”) is Supabase.
---
## 5. **Auth.js (formerly NextAuth) + your own backend**
- **What it is:** The same library NextAuth uses; you run it on **Express**, **Fastify**, or another Node server. Your Vite app is just a client that talks to that API.
- **Pros:** Full control, sessions/callbacks like in Next.js, same mental model as NextAuth.
- **Cons:** You build and run the backend; CORS and cookie/session handling for SPA.
- **Use when:** You want “NextAuth” behavior (sessions, callbacks, multiple providers) with an Express (or similar) API and a Vite React frontend.
---
## Summary
| Need | Good fit |
|-----------------------------|------------------------|
| “Sign in with Google” only | **Firebase Auth** |
| Pre-built UI + dashboard | **Clerk** |
| Backend is Supabase | **Supabase Auth** |
| NextAuth-style + Express | **Auth.js on Express** |
This app uses **Firebase Auth** for a simple, backend-free Google sign-in that works well with Vite + React.

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>grateful-journal</title>
<title>Grateful Journal</title>
</head>
<body>
<div id="root"></div>

1056
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"firebase": "^12.9.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",

View File

@@ -1,42 +1,277 @@
/* Grateful Journal green palette. Responsive layout. */
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
min-height: 100vh;
min-height: 100dvh;
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* ----- Protected route loading ----- */
.protected-route__loading {
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
background: var(--color-bg-soft);
color: var(--color-text-muted);
}
.protected-route__spinner {
width: 32px;
height: 32px;
border: 3px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: login-spin 0.7s linear infinite;
}
@keyframes login-spin {
to { transform: rotate(360deg); }
}
/* ----- Login page green accent aesthetic ----- */
.login-page {
min-height: 100vh;
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(160deg, var(--color-bg-soft) 0%, var(--color-accent-light) 100%);
padding: clamp(0.75rem, 3vw, 1.5rem);
padding-left: max(clamp(0.75rem, 3vw, 1.5rem), env(safe-area-inset-left));
padding-right: max(clamp(0.75rem, 3vw, 1.5rem), env(safe-area-inset-right));
}
.login-page__loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
color: var(--color-text-muted);
}
.login-page__spinner {
width: 32px;
height: 32px;
border: 3px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: login-spin 0.7s linear infinite;
}
/* Login card green accent */
.login-card {
background: var(--color-surface);
padding: clamp(1.5rem, 6vw, 2.75rem);
border-radius: 16px;
border-top: 4px solid var(--color-primary);
box-shadow: 0 8px 32px rgba(27, 230, 44, 0.08), 0 2px 12px rgba(0, 0, 0, 0.04);
max-width: 400px;
width: 100%;
min-width: 0;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
.login-card__brand {
margin-bottom: 1.75rem;
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
.login-card__title {
margin: 0 0 0.5rem;
font-size: clamp(1.5rem, 4.5vw, 1.875rem);
font-weight: 600;
color: var(--color-primary);
letter-spacing: -0.02em;
}
.login-card__tagline {
margin: 0;
color: var(--color-text-muted);
font-size: clamp(0.875rem, 2vw, 0.9375rem);
line-height: 1.5;
}
.login-card__actions {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.login-card__error {
margin: 0;
padding: 0.65rem 0.75rem;
color: #b91c1c;
font-size: 0.875rem;
background: #fef2f2;
border-radius: 8px;
word-break: break-word;
text-align: left;
}
/* Sign in with Google button standard Google-style UI */
.google-sign-in-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
width: 100%;
min-height: 52px;
padding: 0.75rem 1.25rem;
font-size: 1rem;
font-weight: 500;
color: #3c4043;
background: #fff;
border: 1px solid #dadce0;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, box-shadow 0.2s;
-webkit-tap-highlight-color: transparent;
}
.google-sign-in-btn:hover:not(:disabled) {
background: #f8f9fa;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.google-sign-in-btn:active:not(:disabled) {
background: #f1f3f4;
}
.google-sign-in-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.google-sign-in-btn__logo {
flex-shrink: 0;
}
.google-sign-in-btn__label {
color: inherit;
}
/* ----- Home page ----- */
.home-page {
min-height: 100vh;
min-height: 100dvh;
background: var(--color-bg-soft, #f1eee1);
padding: clamp(0.75rem, 3vw, 1.5rem);
padding-left: max(clamp(0.75rem, 3vw, 1.5rem), env(safe-area-inset-left));
padding-right: max(clamp(0.75rem, 3vw, 1.5rem), env(safe-area-inset-right));
}
.home-header {
max-width: 720px;
margin: 0 auto 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
}
.home-header h1 {
margin: 0;
font-size: clamp(1.2rem, 3.5vw, 1.5rem);
color: var(--color-text, #1a1a1a);
word-break: break-word;
}
.home-user {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
min-width: 0;
}
.home-username {
font-weight: 500;
color: var(--color-text, #1a1a1a);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: min(50vw, 180px);
}
.home-sign-out {
min-height: 44px;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
color: var(--color-text-muted, #555);
background: transparent;
border: 1px solid var(--color-border, #cff2dc);
border-radius: 6px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
-webkit-tap-highlight-color: transparent;
}
.home-sign-out:hover {
background: var(--color-surface, #ffffff);
color: var(--color-text, #1a1a1a);
}
.home-main {
max-width: 720px;
margin: 0 auto;
background: var(--color-surface, #ffffff);
padding: clamp(1rem, 4vw, 2rem);
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
min-width: 0;
}
.home-welcome {
margin: 0 0 0.5rem;
font-size: clamp(1.15rem, 3vw, 1.35rem);
color: var(--color-text, #1a1a1a);
word-break: break-word;
}
.home-sub {
margin: 0;
color: var(--color-text-muted, #555);
font-size: clamp(0.875rem, 2vw, 0.95rem);
}
.home-login-link {
display: inline-block;
margin-top: 1rem;
min-height: 48px;
padding: 0.75rem 1.25rem;
line-height: 1.25;
color: #fff;
background: var(--color-primary, #1be62c);
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: background 0.2s;
-webkit-tap-highlight-color: transparent;
}
.home-login-link:hover {
background: var(--color-primary-hover, #18c925);
}
/* Small screens: stack header and tighten spacing */
@media (max-width: 480px) {
.home-header {
flex-direction: column;
align-items: flex-start;
}
to {
transform: rotate(360deg);
.home-user {
width: 100%;
}
.home-username {
max-width: none;
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,34 +1,28 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider } from './contexts/AuthContext'
import { ProtectedRoute } from './components/ProtectedRoute'
import HomePage from './pages/HomePage'
import LoginPage from './pages/LoginPage'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
<AuthProvider>
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
/>
<Route path="/login" element={<LoginPage />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
</BrowserRouter>
</AuthProvider>
)
}

View File

@@ -0,0 +1,53 @@
import type { ButtonHTMLAttributes } from 'react'
/** Google "G" logo standard multicolor mark */
function GoogleLogo({ className }: { className?: string }) {
return (
<svg
className={className}
width="20"
height="20"
viewBox="0 0 24 24"
aria-hidden
>
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
)
}
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
loading?: boolean
}
export function GoogleSignInButton({ loading = false, disabled, className = '', ...rest }: Props) {
return (
<button
type="button"
className={`google-sign-in-btn ${className}`.trim()}
disabled={disabled ?? loading}
aria-busy={loading}
aria-label={loading ? 'Signing in…' : 'Sign in with Google'}
{...rest}
>
<GoogleLogo className="google-sign-in-btn__logo" />
<span className="google-sign-in-btn__label">
{loading ? 'Signing in…' : 'Sign in with Google'}
</span>
</button>
)
}

View File

@@ -0,0 +1,21 @@
import type { ReactNode } from 'react'
type Props = {
title: string
tagline?: string
children: ReactNode
}
export function LoginCard({ title, tagline, children }: Props) {
return (
<div className="login-card">
<div className="login-card__brand">
<h1 className="login-card__title">{title}</h1>
{tagline && <p className="login-card__tagline">{tagline}</p>}
</div>
<div className="login-card__actions">
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,27 @@
import { useEffect, type ReactNode } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
type Props = {
children: ReactNode
}
export function ProtectedRoute({ children }: Props) {
const { user, loading } = useAuth()
const location = useLocation()
if (loading) {
return (
<div className="protected-route__loading" aria-live="polite">
<span className="protected-route__spinner" aria-hidden />
<p>Loading</p>
</div>
)
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />
}
return <>{children}</>
}

View File

@@ -0,0 +1,68 @@
import {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from 'react'
import {
browserLocalPersistence,
onAuthStateChanged,
setPersistence,
signInWithPopup,
signOut as firebaseSignOut,
type User,
} from 'firebase/auth'
import { auth, googleProvider } from '../lib/firebase'
type AuthContextValue = {
user: User | null
loading: boolean
signInWithGoogle: () => Promise<void>
signOut: () => Promise<void>
}
const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (u) => {
setUser(u)
setLoading(false)
})
return () => unsubscribe()
}, [])
async function signInWithGoogle() {
await setPersistence(auth, browserLocalPersistence)
await signInWithPopup(auth, googleProvider)
}
async function signOut() {
await firebaseSignOut(auth)
}
const value: AuthContextValue = {
user,
loading,
signInWithGoogle,
signOut,
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (ctx == null) {
throw new Error('useAuth must be used within AuthProvider')
}
return ctx
}

View File

@@ -1,12 +1,29 @@
/* Grateful Journal green palette from Coolors */
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.5;
font-weight: 400;
/* Responsive base: 16px at 320px, scales up to 18px by 768px */
font-size: clamp(1rem, 0.9rem + 0.25vw, 1.125rem);
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
--color-primary: #1be62c;
--color-primary-hover: #18c925;
--color-bg-soft: #f1eee1;
--color-surface: #ffffff;
--color-accent-light: #cff2dc;
--color-accent-bright: #c3fd2f;
--color-text: #1a1a1a;
--color-text-muted: #555;
--color-border: #cff2dc;
color: var(--color-text);
background-color: var(--color-bg-soft);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
@@ -15,54 +32,37 @@
a {
font-weight: 500;
color: #646cff;
color: var(--color-primary);
text-decoration: inherit;
}
a:hover {
color: #535bf2;
color: var(--color-primary-hover);
}
html {
overflow-x: hidden;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-width: 280px;
min-height: 100vh;
min-height: 100dvh;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
h1 {
font-size: 3.2em;
line-height: 1.1;
font-size: 1.75rem;
line-height: 1.2;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}

16
src/lib/firebase.ts Normal file
View File

@@ -0,0 +1,16 @@
import { initializeApp } from 'firebase/app'
import { getAuth, GoogleAuthProvider } from 'firebase/auth'
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
}
const app = initializeApp(firebaseConfig)
export const auth = getAuth(app)
export const googleProvider = new GoogleAuthProvider()

47
src/pages/HomePage.tsx Normal file
View File

@@ -0,0 +1,47 @@
import { useAuth } from '../contexts/AuthContext'
import { Link } from 'react-router-dom'
export default function HomePage() {
const { user, loading, signOut } = useAuth()
if (loading) {
return (
<div className="home-page">
<p>Loading</p>
</div>
)
}
if (!user) {
return (
<div className="home-page">
<h1>Grateful Journal</h1>
<p>Sign in to start your journal.</p>
<Link to="/login" className="home-login-link">
Go to login
</Link>
</div>
)
}
const displayName =
user.displayName ?? user.email ?? 'there'
return (
<div className="home-page">
<header className="home-header">
<h1>Grateful Journal</h1>
<div className="home-user">
<span className="home-username">{displayName}</span>
<button type="button" className="home-sign-out" onClick={() => signOut()}>
Sign out
</button>
</div>
</header>
<main className="home-main">
<p className="home-welcome">Hello, {displayName}.</p>
<p className="home-sub">Your writing space will go here.</p>
</main>
</div>
)
}

61
src/pages/LoginPage.tsx Normal file
View File

@@ -0,0 +1,61 @@
import { useAuth } from '../contexts/AuthContext'
import { useNavigate } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { GoogleSignInButton } from '../components/GoogleSignInButton'
import { LoginCard } from '../components/LoginCard'
export default function LoginPage() {
const { user, loading, signInWithGoogle } = useAuth()
const navigate = useNavigate()
const [signingIn, setSigningIn] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (loading) return
if (user) {
navigate('/', { replace: true })
}
}, [user, loading, navigate])
async function handleGoogleSignIn() {
setError(null)
setSigningIn(true)
try {
await signInWithGoogle()
} catch (e) {
setError(e instanceof Error ? e.message : 'Sign-in failed')
} finally {
setSigningIn(false)
}
}
if (loading) {
return (
<div className="login-page">
<div className="login-page__loading" aria-live="polite">
<span className="login-page__spinner" aria-hidden />
<p>Loading</p>
</div>
</div>
)
}
return (
<div className="login-page">
<LoginCard
title="Grateful Journal"
tagline="A minimal, private space for gratitude and reflection. No feeds, no noise—just you and your thoughts."
>
<GoogleSignInButton
loading={signingIn}
onClick={handleGoogleSignIn}
/>
{error && (
<p className="login-card__error" role="alert">
{error}
</p>
)}
</LoginCard>
</div>
)
}