├── styles ├── globals.sass └── Home.module.css ├── .eslintrc ├── README.md ├── public ├── favicon.ico └── vercel.svg ├── utils ├── capitalize.js ├── getCategories.js ├── supabaseClientAdmin.js ├── supabaseClient.js ├── isProfileExists.js └── getPosts.js ├── pages ├── _app.js ├── api │ ├── hello.js │ ├── posts │ │ ├── delete.js │ │ ├── update.js │ │ └── create.js │ └── replies │ │ ├── delete.js │ │ ├── update.js │ │ └── create.js ├── posts │ ├── create.js │ ├── index.js │ ├── search.js │ ├── tag │ │ └── [tag].js │ └── [slug].js ├── login.js ├── user │ └── [username].js ├── index.js └── profile.js ├── next.config.js ├── .gitignore ├── package.json ├── components ├── Avatar.js ├── AvatarForm.js ├── Auth.js ├── PostList.js ├── PostForm.js ├── Layout.js └── Reply.js └── yarn.lock /styles/globals.sass: -------------------------------------------------------------------------------- 1 | @import 'bulma/bulma' -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Check out [discussbase website](https://discussbase.vercel.app/) -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hilmanski/discussbase/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /utils/capitalize.js: -------------------------------------------------------------------------------- 1 | export default function capitalize([first, ...rest]) { 2 | return first.toUpperCase() + rest.join('').toLowerCase(); 3 | } -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.sass' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | images: { 4 | domains: ['', 'pbs.twimg.com','camo.githubusercontent.com', 'ui-avatars.com'], 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /pages/posts/create.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import PostForm from '../../components/PostForm' 3 | 4 | export default function Create() { 5 | const router = useRouter() 6 | 7 | return ( 8 | 9 | ) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /utils/getCategories.js: -------------------------------------------------------------------------------- 1 | export default function getCategories(){ 2 | return [ 3 | { key: 'help', name: 'Help 🤸🏼‍♂️', desc: 'ask something' }, 4 | { key: 'show', name: 'Show 🏋🏼‍♀️', desc: "share what you've made" }, 5 | { key: 'random', name: 'Random 🧜🏼', desc: 'Post random things' }, 6 | ] 7 | } -------------------------------------------------------------------------------- /utils/supabaseClientAdmin.js: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL 4 | const supabaseServiceKey = process.env.NEXT_PUBLIC_SUPABASE_SERVICE_KEY 5 | //const JWTSecret = process.env.JWT_SECRET 6 | 7 | export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey) -------------------------------------------------------------------------------- /utils/supabaseClient.js: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL 4 | const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY 5 | const JWTSecret = process.env.JWT_SECRET 6 | 7 | export const supabase = createClient(supabaseUrl, supabaseAnonKey, { 8 | headers: { 9 | Authorization: `Bearer ${JWTSecret}` 10 | } 11 | }) -------------------------------------------------------------------------------- /pages/posts/index.js: -------------------------------------------------------------------------------- 1 | import PostList from '../../components/PostList'; 2 | import getPosts from '../../utils/getPosts'; 3 | 4 | export default function Home({posts, count}) { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export async function getServerSideProps(context) { 11 | 12 | const {posts, count} = await getPosts(context) 13 | 14 | return { 15 | props: { 16 | posts, count 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /pages/posts/search.js: -------------------------------------------------------------------------------- 1 | import PostList from '../../components/PostList'; 2 | import getPosts from '../../utils/getPosts'; 3 | 4 | export default function Search({ posts, count }) { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export async function getServerSideProps(context) { 11 | const { posts, count } = await getPosts(context) 12 | 13 | return { 14 | props: { 15 | posts, count 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /pages/posts/tag/[tag].js: -------------------------------------------------------------------------------- 1 | import PostList from '../../../components/PostList'; 2 | import getPosts from '../../../utils/getPosts'; 3 | 4 | export default function PostByTag({ posts, tag,count }) { 5 | 6 | return ( 7 | 8 | ) 9 | } 10 | 11 | export async function getServerSideProps(context) { 12 | const { posts, count, tag } = await getPosts(context) 13 | 14 | return { 15 | props: { 16 | tag, posts, count 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /pages/api/posts/delete.js: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../../utils/supabaseClient' 2 | 3 | const deletePost = async(req, res) => { 4 | const { access_token, post_id } = req.body; 5 | 6 | try { 7 | supabase.auth.setAuth(access_token) 8 | const { data, error } = await supabase 9 | .from('posts') 10 | .delete() 11 | .match({ id: post_id }) 12 | 13 | res.status(200).json({ status: 'success' }) 14 | } catch (e) { 15 | res.status(500).json({ error: e.message }); 16 | } 17 | } 18 | export default deletePost -------------------------------------------------------------------------------- /utils/isProfileExists.js: -------------------------------------------------------------------------------- 1 | import { supabase } from "./supabaseClient"; 2 | 3 | export default async function isProfileExists() { 4 | try { 5 | const user = supabase.auth.user() 6 | 7 | let { data, error, status } = await supabase 8 | .from('profiles') 9 | .select(`username, website, avatar_url`) 10 | .eq('id', user.id) 11 | .single() 12 | 13 | if (error) { 14 | return false 15 | } 16 | 17 | return true 18 | } catch (error) { 19 | console.log(error.message) 20 | } 21 | } -------------------------------------------------------------------------------- /pages/api/replies/delete.js: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../../utils/supabaseClient' 2 | 3 | const deleteReply = async (req, res) => { 4 | const { access_token, reply_id } = req.body; 5 | 6 | try { 7 | supabase.auth.setAuth(access_token) 8 | const { data, error } = await supabase 9 | .from('replies') 10 | .delete() 11 | .match({ id: reply_id }) 12 | 13 | res.status(200).json({ status: 'success' }) 14 | } catch (e) { 15 | res.status(500).json({ error: e.message }); 16 | } 17 | } 18 | 19 | export default deleteReply -------------------------------------------------------------------------------- /pages/api/posts/update.js: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../../utils/supabaseClient' 2 | 3 | const updatePost = async(req, res) => { 4 | const { title, body, tag, access_token, slug } = req.body; 5 | 6 | try { 7 | supabase.auth.setAuth(access_token) 8 | const { data, error } = await supabase 9 | .from('posts') 10 | .update({ 11 | title: title, 12 | body: body, 13 | tag: tag 14 | }) 15 | .match({ slug }) 16 | 17 | res.status(200).json({ slug }) 18 | } catch (e) { 19 | res.status(500).json({ error: e.message }); 20 | } 21 | } 22 | 23 | export default updatePost -------------------------------------------------------------------------------- /pages/login.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { supabase } from '../utils/supabaseClient' 3 | import Auth from '../components/Auth' 4 | import Layout from '../components/Layout' 5 | import { useRouter } from 'next/router' 6 | 7 | export default function Login() { 8 | const router = useRouter() 9 | const [session, setSession] = useState(supabase.auth.session()) 10 | 11 | if (session) 12 | router.push('/posts') 13 | 14 | useEffect(() => { 15 | supabase.auth.onAuthStateChange((_event, session) => { 16 | setSession(session) 17 | }) 18 | }, []) 19 | 20 | return ( 21 | 22 | {!session && < Auth /> } 23 | 24 | ) 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supabase-discuss", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@supabase/supabase-js": "^1.21.0", 13 | "bulma": "^0.9.3", 14 | "jwt-decode": "^3.1.2", 15 | "next": "11.1.0", 16 | "react": "17.0.2", 17 | "react-dom": "17.0.2", 18 | "react-hook-form": "^7.12.2", 19 | "react-markdown": "^6.0.3", 20 | "react-timeago": "^6.2.1", 21 | "sass": "^1.37.5", 22 | "slugify": "^1.6.0", 23 | "uuid": "^8.3.2" 24 | }, 25 | "devDependencies": { 26 | "eslint": "7.32.0", 27 | "eslint-config-next": "11.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pages/api/replies/update.js: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../../utils/supabaseClient' 2 | import jwt_decode from "jwt-decode"; 3 | 4 | const updateReply = async(req, res) => { 5 | const { body, reply_id, access_token } = req.body; 6 | 7 | const decoded = jwt_decode(access_token); 8 | const user_id = decoded.sub 9 | 10 | try { 11 | supabase.auth.setAuth(access_token) 12 | const { data, error } = await supabase 13 | .from('replies') 14 | .update({ body: body }) 15 | .match({ id: reply_id }) 16 | 17 | let reply = data[0] 18 | 19 | res.status(200).json({ status: 'success', reply: reply }) 20 | } catch (e) { 21 | res.status(500).json({ error: e.message }); 22 | } 23 | } 24 | 25 | export default updateReply -------------------------------------------------------------------------------- /pages/api/posts/create.js: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../../utils/supabaseClient' 2 | import jwt_decode from "jwt-decode"; 3 | const slugify = require('slugify') 4 | 5 | const post = async (req, res) => { 6 | const { title, body, tag, access_token } = req.body; 7 | 8 | const decoded = jwt_decode(access_token); 9 | const user_id = decoded.sub 10 | 11 | const slug = slugify(title) + '-' + Date.now() 12 | 13 | try { 14 | supabase.auth.setAuth(access_token) 15 | const { error } = await supabase 16 | .from('posts') 17 | .insert({ 18 | title: title, 19 | body: body, 20 | tag: tag, 21 | user_id: user_id, 22 | slug: slug 23 | }, { returning: "minimal" }) 24 | 25 | res.status(200).json({ slug }) 26 | } catch (e) { 27 | res.status(500).json({ error: e.message }); 28 | } 29 | } 30 | 31 | export default post -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /pages/api/replies/create.js: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../../utils/supabaseClient' 2 | import { supabaseAdmin } from '../../../utils/supabaseClientAdmin' 3 | import jwt_decode from "jwt-decode"; 4 | 5 | const createReply = async(req, res) => { 6 | const { body, post_id, access_token } = req.body; 7 | 8 | const decoded = jwt_decode(access_token); 9 | const user_id = decoded.sub 10 | 11 | try { 12 | supabase.auth.setAuth(access_token) 13 | const { data, error } = await supabase 14 | .from('replies') 15 | .insert({ 16 | body: body, 17 | user_id: user_id, 18 | post_id: post_id 19 | }) 20 | 21 | let reply = data[0] 22 | if(!error) { 23 | const { error: errsetTimestamp } = await supabaseAdmin 24 | .rpc('set_timestamp', { post_id }) 25 | console.log(errsetTimestamp) 26 | } 27 | 28 | res.status(200).json({ status: 'success', reply: reply }) 29 | } catch (e) { 30 | res.status(500).json({ error: e.message }); 31 | } 32 | } 33 | 34 | export default createReply -------------------------------------------------------------------------------- /utils/getPosts.js: -------------------------------------------------------------------------------- 1 | import { supabase } from "./supabaseClient" 2 | 3 | export default async function getPosts(context) { 4 | let tag = '*' 5 | let searchQ = '' 6 | let _currentPage = 1 7 | 8 | if (context.query.page) { 9 | _currentPage = context.query.page 10 | } 11 | 12 | if (context.query.query) { 13 | searchQ = context.query.query 14 | } 15 | 16 | if (context.query.tag) { 17 | tag = context.params.tag 18 | } 19 | 20 | let perPage = 14 //start from zero 21 | let range_start = 0 22 | let range_end = _currentPage * perPage 23 | 24 | if (_currentPage != 1) 25 | range_start = (_currentPage * perPage) - (perPage - 1) 26 | 27 | const { data: posts, error, count } = await supabase 28 | .from('posts') 29 | .select(` 30 | *, 31 | owner:user_id( 32 | id, username, avatar_url 33 | ), 34 | replies(id) 35 | `, { count: 'exact' }) 36 | .ilike('title', `%${searchQ}%`) 37 | .ilike('tag', `${tag}`) 38 | .range(range_start, range_end) 39 | .order('updated_at', { ascending: false }) 40 | 41 | return { posts, count, tag } 42 | } -------------------------------------------------------------------------------- /components/Avatar.js: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { useState, useEffect } from 'react' 3 | import { supabase } from '../utils/supabaseClient' 4 | 5 | 6 | export default function Avatar({username, avatar_url, size=64}) { 7 | const [avatarUrl, setAvatarUrl] = useState(null) 8 | 9 | useEffect(() => { 10 | if (avatar_url) { 11 | downloadImage(avatar_url) 12 | } 13 | }, [avatar_url]) 14 | 15 | async function downloadImage(path) { 16 | try { 17 | const { data, error } = await supabase.storage.from('avatars').download(path) 18 | if (error) { 19 | throw error 20 | } 21 | const url = URL.createObjectURL(data) 22 | setAvatarUrl(url) 23 | } catch (error) { 24 | console.log('Error downloading image: ', error.message) 25 | } 26 | } 27 | 28 | return ( 29 | <> 30 |
31 | { avatarUrl 32 | ? Avatar 33 | : Avatar 35 | } 36 |
37 | 38 | ) 39 | } -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /components/AvatarForm.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { supabase } from '../utils/supabaseClient' 3 | import Avatar from './Avatar' 4 | 5 | export default function AvatarForm({ username, avatar_url, onUpload }) { 6 | const [uploading, setUploading] = useState(false) 7 | 8 | async function uploadAvatar(event) { 9 | try { 10 | setUploading(true) 11 | 12 | if (!event.target.files || event.target.files.length === 0) { 13 | throw new Error('You must select an image to upload.') 14 | } 15 | 16 | const file = event.target.files[0] 17 | const fileExt = file.name.split('.').pop() 18 | const fileName = `${Math.random()}.${fileExt}` 19 | const filePath = `${fileName}` 20 | 21 | //Size validation 22 | let fileSize = file.size / 1024 / 1024; 23 | fileSize = fileSize.toFixed(2); 24 | console.log(fileSize) 25 | if (fileSize > 2) { 26 | alert('to big, maximum is 2MiB. You file size is: ' + fileSize + ' MiB'); 27 | return 28 | } 29 | 30 | 31 | let { error: uploadError } = await supabase.storage 32 | .from('avatars') 33 | .upload(filePath, file) 34 | 35 | if (uploadError) { 36 | throw uploadError 37 | } 38 | 39 | onUpload(filePath) 40 | } catch (error) { 41 | alert(error.message) 42 | } finally { 43 | setUploading(false) 44 | } 45 | } 46 | 47 | return ( 48 |
49 |
50 | 51 | 52 |
53 | 54 |
55 | 58 | 69 |
70 |
71 | ) 72 | } -------------------------------------------------------------------------------- /pages/user/[username].js: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../utils/supabaseClient' 2 | import Layout from '../../components/Layout'; 3 | import Link from 'next/link'; 4 | import Avatar from '../../components/Avatar'; 5 | 6 | export default function PostByTag({ user }) { 7 | return ( 8 | 9 |
10 |
11 |

Profile

12 | 13 |

@{user.username}

14 | {user.website && 15 |

{user.website}

16 | } 17 | 18 |
19 |

Post

20 | {user.posts.map((post, index) => ( 21 |
  • 22 | {post.title} 23 |
  • 24 | ))} 25 |
    26 | 27 |
    28 |

    Replies

    29 | {user.replies.map((reply, index) => ( 30 |
  • 31 | {reply.body.substr(0, 50)}... 32 |
  • 33 | ))} 34 |
    35 |
    36 |
    37 |
    38 | ) 39 | } 40 | 41 | export async function getServerSideProps(context) { 42 | const { data: user, error } = await supabase 43 | .from('profiles') 44 | .select(` 45 | *, 46 | posts!user_id (user_id, title, slug, created_at), 47 | replies(post_id, body, created_at, posts(slug)) 48 | `) 49 | .eq('username', context.params.username) 50 | .order('created_at', { ascending: false, foreignTable: 'posts' }) 51 | .order('created_at', { ascending: false, foreignTable: 'replies' }) 52 | .single() 53 | 54 | console.log(error) 55 | 56 | if (!user) { 57 | return { 58 | notFound: true, 59 | } 60 | } 61 | 62 | return { 63 | props: { 64 | user 65 | }, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /components/Auth.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { supabase } from '../utils/supabaseClient' 3 | 4 | export default function Auth() { 5 | const [loading, setLoading] = useState(false) 6 | const [email, setEmail] = useState('') 7 | 8 | const handleLogin = async (email) => { 9 | try { 10 | setLoading(true) 11 | const { error } = await supabase.auth.signIn({ email }) 12 | if (error) throw error 13 | alert('Check your email for the login link! Look at spam folder if not in inbox.') 14 | } catch (error) { 15 | alert(error.error_description || error.message) 16 | } finally { 17 | setLoading(false) 18 | } 19 | } 20 | 21 | async function signInWithSocial(provider) { 22 | const { user, session, error } = await supabase.auth.signIn({ 23 | provider: provider 24 | }); 25 | } 26 | 27 | return ( 28 |
    29 |
    30 |

    Join the discussion

    31 |
    32 |

    Social Login

    33 | 34 | 35 |
    36 | 37 | 38 |
    39 |

    Or via email

    40 |
    41 | setEmail(e.target.value)} 47 | /> 48 |
    49 |
    50 | 60 |
    61 |
    62 |
    63 |
    64 | ) 65 | } -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout' 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | 5 | export default function Home() { 6 | 7 | return ( 8 | 9 |

    Discussbase

    10 |

    An open-source forum

    11 | Create your discussion platform and deploy it completely free, 12 | Using “Seven-stack” (Supabase, Vercel and Next.js). 13 | Simplicity first. Focus on your discussion 14 |

    15 | 16 |
    17 | 18 | 19 |
    20 | 21 |
    22 |
    23 |
    24 | supabase logo 25 |
    26 |

    Supabase

    27 |

    Create a backend in less than 2 minutes. Start your project with a Postgres Database, Authentication, instant APIs, realtime subscriptions and Storage.

    28 |
    29 | 30 |
    31 |
    32 | vercel logo 33 |
    34 |

    Vercel

    35 |

    Vercel is an open serverless platform for static and hybrid applications built to integrate with your headless content, commerce, or database.

    36 |
    37 | 38 |
    39 |
    40 | nextjs logo 41 |
    42 |

    Nextjs

    43 |

    Next.js gives you the best developer experience with all the features you need for production.

    44 |
    45 |
    46 | 47 | Star on Github 48 | 49 |
    50 | ) 51 | } -------------------------------------------------------------------------------- /components/PostList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import Layout from '../components/Layout'; 3 | import Link from 'next/link'; 4 | import Head from 'next/head' 5 | import TimeAgo from 'react-timeago' 6 | import capitalize from '../utils/capitalize'; 7 | import Avatar from './Avatar'; 8 | import getCategories from '../utils/getCategories'; 9 | 10 | export default function PostList({ posts, totalPosts, tag = null }) { 11 | const [pathName, setPathName] = useState(null) 12 | const perPage = 15 13 | const totalPage = Math.ceil(totalPosts/perPage) 14 | const categories = getCategories() 15 | const category = categories.find(item => item.key === tag); 16 | 17 | useEffect(() => { 18 | // component is mounted and window is available 19 | setPathName(window.location.pathname) 20 | }, []); 21 | 22 | return ( 23 | 24 | 25 | All Posts {category && category.name} 26 | 27 |
    28 |
    29 |

    Post {category && category.name}

    30 |

    31 | {category ? category.desc : 'Everything in this forum' } 32 |

    33 |
    34 | 35 |
    36 |
    37 |

    Categories

    38 | 39 | All 40 | 41 | {categories.map((cat, index) => ( 42 | 43 | {cat.name} 44 | 45 | ))} 46 |
    47 | 48 |
    49 | 50 |
    51 |
    52 | 53 |
    54 |
    55 | 56 |
    57 |
    58 | 59 | {posts.map((post) => ( 60 |
    61 |
    62 | 63 |
    64 | 65 |
    66 |

    67 | {capitalize(post.title)} 68 | 69 |

    70 |

    71 | 72 | @{post.owner.username} 73 | 74 | posted 75 | | {post.replies.length} comments 76 |

    77 |
    78 |
    79 | ))} 80 | 81 | {totalPage > 1 && 82 |