This commit is contained in:
Mattias Wiberg 2025-06-02 21:48:40 +02:00
commit 5aa9b32c03
27 changed files with 4142 additions and 0 deletions

64
pages/_app.js Normal file
View file

@ -0,0 +1,64 @@
import '~/styles/style.scss'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
import UserContext from 'lib/UserContext'
import { supabase } from 'lib/Store'
import { jwtDecode } from 'jwt-decode'
export default function SupabaseSlackClone({ Component, pageProps }) {
const [userLoaded, setUserLoaded] = useState(false)
const [user, setUser] = useState(null)
const [session, setSession] = useState(null)
const router = useRouter()
useEffect(() => {
function saveSession(
/** @type {Awaited<ReturnType<typeof supabase.auth.getSession>>['data']['session']} */
session
) {
setSession(session)
const currentUser = session?.user
if (session) {
const jwt = jwtDecode(session.access_token)
currentUser.appRole = jwt.user_role
}
setUser(currentUser ?? null)
setUserLoaded(!!currentUser)
if (currentUser) {
router.push('/channels/[id]', '/channels/1')
}
}
supabase.auth.getSession().then(({ data: { session } }) => saveSession(session))
const { data: { subscription: authListener } } = supabase.auth.onAuthStateChange(
async (event, session) => {
console.log(session)
saveSession(session)
}
)
return () => {
authListener.unsubscribe()
}
}, [])
const signOut = async () => {
const { error } = await supabase.auth.signOut()
if (!error) {
router.push('/')
}
}
return (
<UserContext.Provider
value={{
userLoaded,
user,
signOut,
}}
>
<Component {...pageProps} />
</UserContext.Provider>
)
}

52
pages/channels/[id].js Normal file
View file

@ -0,0 +1,52 @@
import Layout from '~/components/Layout'
import Message from '~/components/Message'
import MessageInput from '~/components/MessageInput'
import { useRouter } from 'next/router'
import { useStore, addMessage } from '~/lib/Store'
import { useContext, useEffect, useRef } from 'react'
import UserContext from '~/lib/UserContext'
const ChannelsPage = (props) => {
const router = useRouter()
const { user, authLoaded, signOut } = useContext(UserContext)
const messagesEndRef = useRef(null)
// Else load up the page
const { id: channelId } = router.query
const { messages, channels } = useStore({ channelId })
useEffect(() => {
messagesEndRef.current.scrollIntoView({
block: 'start',
behavior: 'smooth',
})
}, [messages])
// redirect to public channel when current channel is deleted
useEffect(() => {
if (!channels.some((channel) => channel.id === Number(channelId))) {
router.push('/channels/1')
}
}, [channels, channelId])
// Render the channels and messages
return (
<Layout channels={channels} activeChannelId={channelId}>
<div className="relative h-screen">
<div className="Messages h-full pb-16">
<div className="p-2 overflow-y-auto">
{messages.map((x) => (
<Message key={x.id} message={x} />
))}
<div ref={messagesEndRef} style={{ height: 0 }} />
</div>
</div>
<div className="p-2 absolute bottom-0 left-0 w-full">
<MessageInput onSubmit={async (text) => addMessage(text, channelId, user.id)} />
</div>
</div>
</Layout>
)
}
export default ChannelsPage

79
pages/index.js Normal file
View file

@ -0,0 +1,79 @@
import { useState } from 'react'
import { supabase } from 'lib/Store'
const Home = () => {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const handleLogin = async (type, username, password) => {
try {
const { error, data: { user } } =
type === 'LOGIN'
? await supabase.auth.signInWithPassword({ email: username, password })
: await supabase.auth.signUp({ email: username, password })
// If the user doesn't exist here and an error hasn't been raised yet,
// that must mean that a confirmation email has been sent.
// NOTE: Confirming your email address is required by default.
if (error) {
alert('Error with auth: ' + error.message)
} else if (!user) alert('Signup successful, confirmation mail should be sent soon!')
} catch (error) {
console.log('error', error)
alert(error.error_description || error)
}
}
return (
<div className="w-full h-full flex justify-center items-center p-4 bg-gray-300">
<div className="w-full sm:w-1/2 xl:w-1/3">
<div className="border-teal p-8 border-t-12 bg-white mb-6 rounded-lg shadow-lg bg-white">
<div className="mb-4">
<label className="font-bold text-grey-darker block mb-2">Email</label>
<input
type="text"
className="block appearance-none w-full bg-white border border-grey-light hover:border-grey px-2 py-2 rounded shadow"
placeholder="Your Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="mb-4">
<label className="font-bold text-grey-darker block mb-2">Password</label>
<input
type="password"
className="block appearance-none w-full bg-white border border-grey-light hover:border-grey px-2 py-2 rounded shadow"
placeholder="Your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<a
onClick={(e) => {
e.preventDefault()
handleLogin('SIGNUP', username, password)
}}
href={'/channels'}
className="bg-indigo-700 hover:bg-teal text-white py-2 px-4 rounded text-center transition duration-150 hover:bg-indigo-600 hover:text-white"
>
Sign up
</a>
<a
onClick={(e) => {
e.preventDefault()
handleLogin('LOGIN', username, password)
}}
href={'/channels'}
className="border border-indigo-700 text-indigo-700 py-2 px-4 rounded w-full text-center transition duration-150 hover:bg-indigo-700 hover:text-white"
>
Login
</a>
</div>
</div>
</div>
</div>
)
}
export default Home