├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── common │ ├── PageHead.tsx │ ├── PageHeading.tsx │ └── PageSection.tsx ├── footers │ └── RootFooter.tsx ├── headers │ └── RootHeader.tsx ├── interactive │ ├── ColorSwitcher.tsx │ └── LanguageSwitcher.tsx ├── layouts │ ├── DashboardLayout.tsx │ └── RootLayout.tsx ├── navigations │ ├── DashboardNavigation.tsx │ └── RootNavigation.tsx └── ui │ ├── Popover.tsx │ └── Tooltip.tsx ├── helpers ├── globalStyles.ts ├── initPocketbase.ts ├── isOnline.ts └── theme.ts ├── hooks └── useSaveLocaleCookie.ts ├── i18n.json ├── locales ├── de │ └── common.json └── en │ └── common.json ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── dashboard │ ├── index.tsx │ └── settings │ │ └── index.tsx ├── index.tsx ├── login │ └── index.tsx └── user │ ├── [slug] │ ├── edit │ │ └── index.tsx │ └── index.tsx │ ├── index.tsx │ └── me │ ├── edit │ └── index.tsx │ └── index.tsx ├── postcss.config.js ├── public └── favicon.ico ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── types └── user.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # VS Code 39 | /.vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js without app directory starter kit 2 | 3 | This is my boilerplate for the stable Next.js version without app directory. It is mainly powered by Pocketbase. 4 | 5 | ## Main Features 6 | 7 | - Next.js for frontend 8 | - Pocketbase for backend 9 | - Pocketbase for database 10 | - Pocketbase for authentication 11 | - Pocketbase for image hosting 12 | 13 | ## Other features 14 | 15 | - NextUI 16 | - TailwindCSS 17 | - NextUI 18 | - React-Icons 19 | - Dark-Mode with NextUI and next-themes 20 | - Internationalization with next-translate 21 | - Formik and Yup for easy and hasslefree forms and form validation (Coming Soon) 22 | - Custom authentication flow with Pocketbase 23 | - Custom basic user system (WIP) 24 | -------------------------------------------------------------------------------- /components/common/PageHead.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import useTranslation from "next-translate/useTranslation"; 3 | 4 | export default function PageHead({ title }: { title: string }) { 5 | const { t } = useTranslation("common"); 6 | 7 | return ( 8 | 9 | {t("sitename") + " | " + title} 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components/common/PageHeading.tsx: -------------------------------------------------------------------------------- 1 | //Import Components 2 | import { Container, Text } from "@nextui-org/react"; 3 | 4 | export default function PageHeading({ text }: { text: string }) { 5 | return ( 6 | 7 | {text} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /components/common/PageSection.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import { Grid, Row, Text } from "@nextui-org/react"; 3 | 4 | type Props = { 5 | title: string; 6 | children: React.ReactNode; 7 | }; 8 | 9 | export default function PageSection({ title, children }: Props) { 10 | return ( 11 | 12 | 13 | {title} 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/footers/RootFooter.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from "@nextui-org/react"; 2 | 3 | export default function RootFooter() { 4 | return ( 5 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /components/headers/RootHeader.tsx: -------------------------------------------------------------------------------- 1 | import RootNavigation from "../navigations/RootNavigation"; 2 | 3 | export default function RootHeader() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /components/interactive/ColorSwitcher.tsx: -------------------------------------------------------------------------------- 1 | // Imoport components 2 | import { useTheme as useNextTheme } from "next-themes"; 3 | import { Switch, useTheme } from "@nextui-org/react"; 4 | 5 | // Import icons 6 | import { IoMoon, IoSunny } from "react-icons/io5"; 7 | import { useEffect, useState } from "react"; 8 | 9 | // Fire the component 10 | export default function ColorSwitcher({ 11 | size, 12 | shadow, 13 | }: { 14 | size?: "xs" | "sm" | "md" | "lg" | "xl"; 15 | shadow?: boolean; 16 | }) { 17 | const { setTheme } = useNextTheme(); 18 | const { isDark } = useTheme(); 19 | 20 | // Set loading state 21 | const [isLoading, setIsLoading] = useState(true); 22 | 23 | // Set loading state to false when component is mounted 24 | useEffect(() => { 25 | setIsLoading(false); 26 | }, []); 27 | 28 | // Return empty switch if loading 29 | if (isLoading) return ; 30 | 31 | // Return switch 32 | return ( 33 | } 40 | iconOff={} 41 | onChange={(e) => setTheme(e.target.checked ? "dark" : "light")} 42 | /> 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/interactive/LanguageSwitcher.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import useTranslation from "next-translate/useTranslation"; 3 | import setLanguage from "next-translate/setLanguage"; 4 | import { setCookie } from "cookies-next"; 5 | import { Button } from "@nextui-org/react"; 6 | 7 | // Import icons 8 | import { IoLanguage } from "react-icons/io5"; 9 | 10 | // Import i18n config and set locales 11 | import i18nConfig from "../../i18n.json"; 12 | const { locales } = i18nConfig; 13 | 14 | // Fire the component 15 | export default function ChangeLanguage() { 16 | const { t, lang } = useTranslation("common"); 17 | 18 | return ( 19 | // Map though locales and return a button for each 20 | <> 21 | {locales.map((lng) => { 22 | if (lng === lang) return null; 23 | 24 | return ( 25 | 41 | ); 42 | })} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /components/layouts/DashboardLayout.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import { Container } from "@nextui-org/react"; 3 | import DashboardNavigation from "../navigations/DashboardNavigation"; 4 | 5 | // Import types 6 | import type { authData } from "../../types/user"; 7 | 8 | type Props = { 9 | children: React.ReactNode; 10 | authData: { 11 | authData: authData; 12 | isLoggedIn: boolean; 13 | roles: string[]; 14 | }; 15 | }; 16 | 17 | // Fire up the layout 18 | export default function DashboardLayout({ children, authData }: Props) { 19 | return ( 20 | 21 | 28 |
{children}
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/layouts/RootLayout.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import RootFooter from "../footers/RootFooter"; 3 | import RootHeader from "../headers/RootHeader"; 4 | 5 | // Fire up the layout 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 |
13 | 14 | {children} 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/navigations/DashboardNavigation.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | import { Avatar, Badge, Card, Collapse, Text } from "@nextui-org/react"; 5 | import Tooltip from "../ui/Tooltip"; 6 | 7 | // Import icons 8 | import { IoMdSettings } from "react-icons/io"; 9 | import { IoSettingsSharp } from "react-icons/io5"; 10 | import { 11 | FaChevronLeft, 12 | FaUsers, 13 | FaUsersSlash, 14 | FaUserEdit, 15 | } from "react-icons/fa"; 16 | import { MdSpaceDashboard } from "react-icons/md"; 17 | import { BiUserPin } from "react-icons/bi"; 18 | 19 | // Import types 20 | import type { authData } from "../../types/user"; 21 | 22 | type Props = { 23 | authData: authData; 24 | isLoggedIn: boolean; 25 | roles: string[]; 26 | }; 27 | 28 | // Items for main navigation 29 | const mainItems = [ 30 | { name: "Dashboard", href: "/dashboard", icon: }, 31 | { name: "Settings", href: "/dashboard/settings", icon: }, 32 | { name: "Show Profile", href: "/user/me", icon: }, 33 | { name: "Edit Profile", href: "/user/me/edt", icon: }, 34 | ]; 35 | 36 | // Items for user navigation 37 | const userItems = [ 38 | { name: "User List", href: "/user", icon: }, 39 | { name: "Banned Users", href: "/user", icon: }, 40 | ]; 41 | 42 | // Fire up the navigation 43 | export default function DashboardNavigation({ 44 | authData, 45 | isLoggedIn, 46 | roles, 47 | }: Props) { 48 | const router = useRouter(); 49 | 50 | return ( 51 | 52 | 59 | Settings 60 | 61 | } 62 | subtitle={ 63 | 64 | Manage your settings 65 | 66 | } 67 | expanded 68 | arrowIcon={} 69 | contentLeft={ 70 | 96 | } 97 | > 98 | 126 | 127 | 128 | {isLoggedIn && roles && roles.some((item) => ["Superadmin", "Admin"]) && ( 129 | 136 | User Management 137 | 144 | 149 | <>Admin 150 | 151 | 152 | 153 | } 154 | subtitle={ 155 | 156 | Manage user and permissions 157 | 158 | } 159 | arrowIcon={} 160 | > 161 | 189 | 190 | )} 191 | 192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /components/navigations/RootNavigation.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import { Navbar, Text, Badge } from "@nextui-org/react"; 3 | import useTranslation from "next-translate/useTranslation"; 4 | import Link from "next/link"; 5 | import { useRouter } from "next/router"; 6 | import { setCookie } from "cookies-next"; 7 | import ColorSwitcher from "../interactive/ColorSwitcher"; 8 | 9 | const collapseItems = ["Features", "Customers", "Pricing"]; 10 | 11 | export default function RootNavigation() { 12 | const { t } = useTranslation("common"); 13 | const router = useRouter(); 14 | const shortUrl = router.asPath.split("/")[1]; 15 | 16 | return ( 17 | <> 18 | 24 | 25 | 26 | 27 | {t("sitename")} 28 | 29 | 30 | ALPHA 31 | 32 | 33 | 38 | 39 | 40 | Home 41 | 42 | 43 | 44 | 45 | User 46 | 47 | 48 | 49 | 50 | Dashboard 51 | 52 | 53 | 54 | { 57 | setCookie("redirect_after_login", router.asPath); 58 | }} 59 | > 60 | Login 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {collapseItems.map((item) => ( 69 | 70 | 71 | {item} 72 | 73 | 74 | ))} 75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /components/ui/Popover.tsx: -------------------------------------------------------------------------------- 1 | // Import Components 2 | import { Button, Grid, Popover as UIPopover } from "@nextui-org/react"; 3 | 4 | // Import types 5 | import { authData } from "../../types/user"; 6 | 7 | type Props = { 8 | icon?: React.ReactNode; 9 | children: React.ReactNode; 10 | text: string; 11 | authData?: authData; 12 | isOpen: boolean; 13 | setIsOpen: (isOpen: boolean) => void; 14 | width?: string; 15 | offset?: number; 16 | padding?: string; 17 | }; 18 | 19 | export default function Popover({ 20 | children, 21 | icon, 22 | text, 23 | isOpen, 24 | setIsOpen, 25 | width, 26 | offset, 27 | padding, 28 | }: Props) { 29 | return ( 30 | setIsOpen(!isOpen)} 34 | > 35 | 36 | 39 | 40 | 41 | 47 | {children} 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/ui/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import { Tooltip as UITooltip } from "@nextui-org/react"; 3 | 4 | // Import types 5 | import type { ReactElement } from "react"; 6 | 7 | type Props = { 8 | children: ReactElement; 9 | isAlternative?: boolean; 10 | text: string; 11 | alternativeText?: string; 12 | offset?: number; 13 | leaveDelay?: number; 14 | showArrow?: boolean; 15 | color?: 16 | | "default" 17 | | "primary" 18 | | "secondary" 19 | | "success" 20 | | "warning" 21 | | "error" 22 | | "invert"; 23 | textColor?: 24 | | "default" 25 | | "primary" 26 | | "secondary" 27 | | "success" 28 | | "warning" 29 | | "error"; 30 | click?: boolean; 31 | }; 32 | 33 | // Render the tooltip 34 | export default function Tooltip({ 35 | children, 36 | isAlternative, 37 | text, 38 | alternativeText, 39 | offset, 40 | leaveDelay, 41 | showArrow, 42 | color, 43 | textColor, 44 | click, 45 | }: Props) { 46 | return ( 47 | 56 | {children} 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /helpers/globalStyles.ts: -------------------------------------------------------------------------------- 1 | const globalStyles = { 2 | body: { 3 | fontFamily: "Bai Jamjuree, sans-serif", 4 | }, 5 | p: { 6 | fontFamily: "Bai Jamjuree, sans-serif", 7 | }, 8 | h1: { 9 | fontFamily: "Chakra Petch, sans-serif", 10 | }, 11 | h2: { 12 | fontFamily: "Chakra Petch, sans-serif", 13 | }, 14 | h3: { 15 | fontFamily: "Chakra Petch, sans-serif", 16 | }, 17 | h4: { 18 | fontFamily: "Chakra Petch, sans-serif", 19 | }, 20 | h5: { 21 | fontFamily: "Chakra Petch, sans-serif", 22 | }, 23 | h6: { 24 | fontFamily: "Chakra Petch, sans-serif", 25 | }, 26 | }; 27 | 28 | export default globalStyles; 29 | -------------------------------------------------------------------------------- /helpers/initPocketbase.ts: -------------------------------------------------------------------------------- 1 | // Import components 2 | import PocketBase from "pocketbase"; 3 | 4 | // Import types 5 | import type { GetServerSidePropsContext } from "next"; 6 | 7 | export default async function initPocketBase( 8 | context: GetServerSidePropsContext 9 | ) { 10 | const pb = new PocketBase(process.env.NEXT_PUBLIC_API_URL); 11 | 12 | // Load the store data from cookie 13 | pb.authStore.loadFromCookie(context.req?.headers?.cookie || ""); 14 | 15 | // Send new cookie if actual data 16 | pb.authStore.onChange(() => { 17 | context.res?.setHeader("set-cookie", pb.authStore.exportToCookie()); 18 | }); 19 | 20 | try { 21 | // Get the user data from the server if the auth is valid 22 | pb.authStore.isValid && (await pb.collection("users").authRefresh()); 23 | } catch (_) { 24 | // Clear the auth store if the auth is invalid 25 | pb.authStore.clear(); 26 | } 27 | 28 | // Update the last active date 29 | if (pb.authStore.isValid && pb.authStore.model) { 30 | await pb.collection("users").update(pb.authStore.model.id, { 31 | lastActive: new Date(), 32 | }); 33 | } 34 | 35 | return pb; 36 | } 37 | -------------------------------------------------------------------------------- /helpers/isOnline.ts: -------------------------------------------------------------------------------- 1 | // Importing types 2 | import type { authData } from "../types/user"; 3 | 4 | //Fire the function 5 | export default function isOnline({ authData }: { authData: authData }) { 6 | // Return false if the user was not active in the last 5 minutes 7 | const lastActive = Math.floor(new Date(authData.lastActive).getTime() / 1000); 8 | const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5; 9 | if (fiveMinutesAgo - lastActive > 301) return false; 10 | // Return true if the user is online 11 | return true; 12 | } 13 | -------------------------------------------------------------------------------- /helpers/theme.ts: -------------------------------------------------------------------------------- 1 | export const themeLight = { 2 | type: "light", 3 | theme: { 4 | fonts: { 5 | sans: "Bai Jamjuree, sans-serif", 6 | primary: "Bai Jamjuree, sans-serif", 7 | secondary: "Charaka Petch, sans-serif", 8 | }, 9 | }, 10 | }; 11 | 12 | export const themeDark = { 13 | type: "dark", 14 | theme: { 15 | fonts: { 16 | sans: "Bai Jamjuree, sans-serif", 17 | primary: "Bai Jamjuree, sans-serif", 18 | secondary: "Charaka Petch, sans-serif", 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /hooks/useSaveLocaleCookie.ts: -------------------------------------------------------------------------------- 1 | // Import components 2 | import { useRouter } from "next/router"; 3 | import { useEffect } from "react"; 4 | import { getCookie } from "cookies-next"; 5 | 6 | // Fire the hook 7 | export default function useSaveLocaleCookie() { 8 | const { locale, defaultLocale } = useRouter(); 9 | const language = getCookie("NEXT_LOCALE"); 10 | 11 | // Save the locale cookie when the locale changes 12 | useEffect(saveLocaleCookie, [locale, defaultLocale, language]); 13 | 14 | // Save the locale cookie 15 | function saveLocaleCookie() { 16 | if (!language) { 17 | const date = new Date(); 18 | const expire = 30 * 24 * 60 * 60 * 1000; // 30 days 19 | date.setTime(date.getTime() + expire); 20 | document.cookie = `NEXT_LOCALE=${locale};expires=${date.toUTCString()};path=/`; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "locales": ["en", "de"], 3 | "defaultLocale": "en", 4 | "pages": { 5 | "*": ["common"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /locales/de/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "sitename": "Development Vorlage", 3 | "meta_description": "Next.js + NextUI + Tailwind + Pocketbase = Vorlage", 4 | "title": "Next.js + NextUI + Tailwind + Pocketbase = Vorlage", 5 | "de": "German", 6 | "en": "English", 7 | "settings": "Einstellungen" 8 | } 9 | -------------------------------------------------------------------------------- /locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "sitename": "Development Boilerplate", 3 | "meta_description": "Next.js + NextUI + Tailwind + Pocketbase = Boilerplate", 4 | "title": "Next.js + NextUI + Tailwind + Pocketbase = Boilerplate", 5 | "de": "Deutsch", 6 | "en": "English", 7 | "settings": "Settings" 8 | } 9 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | const nextTranslate = require("next-translate"); 7 | 8 | module.exports = nextConfig; 9 | module.exports = nextTranslate(); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-with-pocketbase", 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 | "@fontsource/bai-jamjuree": "^4.5.9", 13 | "@fontsource/chakra-petch": "^4.5.9", 14 | "@next/font": "13.1.0", 15 | "@nextui-org/react": "^1.0.0-beta.11", 16 | "@types/node": "18.11.17", 17 | "@types/react": "18.0.26", 18 | "@types/react-dom": "18.0.10", 19 | "cookies-next": "^2.1.1", 20 | "eslint": "8.30.0", 21 | "eslint-config-next": "13.1.0", 22 | "next": "13.1.0", 23 | "next-themes": "^0.2.1", 24 | "next-translate": "^1.6.0", 25 | "pocketbase": "^0.9.0", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "react-icons": "^4.7.1", 29 | "typescript": "4.9.4" 30 | }, 31 | "devDependencies": { 32 | "autoprefixer": "^10.4.13", 33 | "postcss": "^8.4.20", 34 | "tailwindcss": "^3.2.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | // Import used fonts 2 | import "@fontsource/bai-jamjuree/400.css"; 3 | import "@fontsource/bai-jamjuree/400-italic.css"; 4 | import "@fontsource/bai-jamjuree/600.css"; 5 | import "@fontsource/bai-jamjuree/700.css"; 6 | import "@fontsource/bai-jamjuree/700-italic.css"; 7 | import "@fontsource/chakra-petch/700.css"; 8 | import "@fontsource/chakra-petch/700-italic.css"; 9 | 10 | // Import global css 11 | import "../styles/globals.css"; 12 | 13 | // Import components 14 | import Head from "next/head"; 15 | import { NextUIProvider, createTheme, globalCss } from "@nextui-org/react"; 16 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 17 | import globalStyles from "../helpers/globalStyles"; 18 | import { themeLight, themeDark } from "../helpers/theme"; 19 | import useSaveLocaleCookie from "../hooks/useSaveLocaleCookie"; 20 | 21 | // Import and set types 22 | import type { ReactElement, ReactNode } from "react"; 23 | import type { AppProps } from "next/app"; 24 | import type { NextPage } from "next"; 25 | 26 | export type NextPageWithLayout

= NextPage & { 27 | getLayout?: (page: ReactElement) => ReactNode; 28 | }; 29 | 30 | type AppPropsWithLayout = AppProps & { 31 | Component: NextPageWithLayout; 32 | }; 33 | 34 | // Create custom themes 35 | const lightTheme = createTheme(themeLight); 36 | const darkTheme = createTheme(themeDark); 37 | 38 | // Create global css 39 | const globalStyle = globalCss(globalStyles); 40 | 41 | // Fire the app 42 | export default function MyApp({ Component, pageProps }: AppPropsWithLayout) { 43 | const getLayout = Component.getLayout ?? ((page) => page); 44 | 45 | // Set global css 46 | globalStyle(); 47 | // Save locale cookie 48 | useSaveLocaleCookie(); 49 | 50 | return ( 51 | <> 52 | 53 | 54 | 55 | 56 | 64 | 65 | {getLayout()} 66 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import React from "react"; 3 | import Document, { Html, Head, Main, NextScript } from "next/document"; 4 | import { CssBaseline } from "@nextui-org/react"; 5 | 6 | // Fire the document 7 | class MyDocument extends Document { 8 | static async getInitialProps(ctx: any) { 9 | const initialProps = await Document.getInitialProps(ctx); 10 | return { 11 | ...initialProps, 12 | styles: React.Children.toArray([initialProps.styles]), 13 | }; 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | {CssBaseline.flush()} 20 | 21 |

22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default MyDocument; 30 | -------------------------------------------------------------------------------- /pages/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | //Import components 2 | import Head from "next/head"; 3 | import { Container } from "@nextui-org/react"; 4 | import useTranslation from "next-translate/useTranslation"; 5 | import { getCookie } from "cookies-next"; 6 | import Layout from "../../components/layouts/RootLayout"; 7 | import initPocketBase from "../../helpers/initPocketbase"; 8 | import DashboardLayout from "../../components/layouts/DashboardLayout"; 9 | import PageHeading from "../../components/common/PageHeading"; 10 | 11 | // Import types 12 | import type { ReactElement } from "react"; 13 | import type { GetServerSidePropsContext } from "next"; 14 | import type { authData } from "../../types/user"; 15 | 16 | // Fire the Home page 17 | export default function Dashboard({ 18 | isLoggedIn, 19 | authData, 20 | roles, 21 | }: { 22 | isLoggedIn: boolean; 23 | authData: authData; 24 | roles: string[]; 25 | }) { 26 | const { t } = useTranslation("common"); 27 | 28 | return ( 29 | <> 30 | 31 | {t("sitename") + " | Dashboard"} 32 | 33 | 34 | 35 | Content 36 | 37 | ); 38 | } 39 | 40 | // Export serverside props 41 | export async function getServerSideProps(context: GetServerSidePropsContext) { 42 | // Get the language cookie 43 | const langCookie = getCookie("NEXT_LOCALE", context.res); 44 | 45 | // Init PocketBase 46 | const pb = await initPocketBase(context); 47 | 48 | // Strip the authData from the pb authStore 49 | const authData = await JSON.parse(JSON.stringify(pb.authStore)); 50 | 51 | let userRoles; 52 | let roles; 53 | 54 | // If the user is not logged in, redirect to the login page 55 | if (!pb.authStore.isValid) { 56 | return { 57 | redirect: { 58 | destination: "/login", 59 | permanent: false, 60 | }, 61 | }; 62 | } 63 | 64 | // If the language cookie is not the actual language, redirect to correct language 65 | if (langCookie && langCookie !== context.locale) { 66 | return { 67 | redirect: { 68 | destination: "/" + langCookie + context.resolvedUrl, 69 | permanent: false, 70 | }, 71 | }; 72 | } 73 | 74 | // Get the user roles 75 | await pb 76 | .collection("users") 77 | .getOne(authData.baseModel.id, { 78 | expand: "roles", 79 | }) 80 | .then((res) => { 81 | userRoles = res.expand.roles; 82 | roles = userRoles.map((role: { name: string }) => role.name); 83 | }) 84 | .catch(() => { 85 | roles = []; 86 | }); 87 | 88 | // Return the props if user is logged in and correct language is set 89 | return { 90 | props: { 91 | isLoggedIn: pb.authStore.isValid, 92 | authData: authData.baseModel, 93 | roles: roles, 94 | }, 95 | }; 96 | } 97 | 98 | Dashboard.getLayout = function getLayout(page: ReactElement) { 99 | return ( 100 | 101 | {page} 102 | 103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /pages/dashboard/settings/index.tsx: -------------------------------------------------------------------------------- 1 | //Import components 2 | import { useState } from "react"; 3 | import { useRouter } from "next/router"; 4 | import PocketBase from "pocketbase"; 5 | import { 6 | Container, 7 | Text, 8 | Row, 9 | Switch, 10 | Grid, 11 | Checkbox, 12 | Button, 13 | } from "@nextui-org/react"; 14 | import useTranslation from "next-translate/useTranslation"; 15 | import setLanguage from "next-translate/setLanguage"; 16 | import { deleteCookie, getCookie } from "cookies-next"; 17 | import Layout from "../../../components/layouts/RootLayout"; 18 | import initPocketBase from "../../../helpers/initPocketbase"; 19 | import DashboardLayout from "../../../components/layouts/DashboardLayout"; 20 | import PageHeading from "../../../components/common/PageHeading"; 21 | import Tooltip from "../../../components/ui/Tooltip"; 22 | import Popover from "../../../components/ui/Popover"; 23 | import ColorSwitcher from "../../../components/interactive/ColorSwitcher"; 24 | import PageSection from "../../../components/common/PageSection"; 25 | import PageHead from "../../../components/common/PageHead"; 26 | 27 | // Import types 28 | import type { ReactElement } from "react"; 29 | import type { GetServerSidePropsContext } from "next"; 30 | import type { authData } from "../../../types/user"; 31 | 32 | // Import icons 33 | import { IoWarningOutline } from "react-icons/io5"; 34 | import { AiOutlineUserDelete } from "react-icons/ai"; 35 | 36 | // Fire the dashboard settings page 37 | export default function DashboardSettings({ 38 | isLoggedIn, 39 | authData, 40 | roles, 41 | }: { 42 | isLoggedIn: boolean; 43 | authData: authData; 44 | roles: string[]; 45 | }) { 46 | const { t } = useTranslation("common"); 47 | const router = useRouter(); 48 | 49 | // Set states 50 | const [showEmail, setShowEmail] = useState(authData.emailVisibility); 51 | const [showOnline, setShowOnline] = useState(authData.showOnline); 52 | const [selectedLocale, setSelectedLocale] = useState([router.locale || "en"]); 53 | const [showOnlinePressed, setShowOnlinePressed] = useState(false); 54 | const [showEmailPressed, setShowEmailPressed] = useState(false); 55 | const [isDeletePopoverOpen, setIsDeletePopoverOpen] = useState(false); 56 | 57 | // Connect to database 58 | const pb = new PocketBase(process.env.NEXT_PUBLIC_API_URL); 59 | 60 | return ( 61 | <> 62 | 63 | 64 | 65 | 66 | {/* Show content if logged in */} 67 | {isLoggedIn && ( 68 | <> 69 | {/* Section: Privacy Settings */} 70 | 71 | 72 | 73 | 78 | { 85 | setShowEmailPressed(true); 86 | setShowEmail(!showEmail); 87 | await pb.collection("users").update(authData.id, { 88 | emailVisibility: !showEmail, 89 | }); 90 | setTimeout(() => { 91 | setShowEmailPressed(false); 92 | }, 3000); 93 | }} 94 | /> 95 | 96 | Show my email adress 97 | 98 | 99 | 100 | 105 | { 112 | setShowOnlinePressed(true); 113 | setShowOnline(!showOnline); 114 | await pb.collection("users").update(authData.id, { 115 | showOnline: !showOnline, 116 | }); 117 | router.push(router.asPath, undefined, { 118 | locale: router.locale, 119 | }); 120 | setTimeout(() => { 121 | setShowOnlinePressed(false); 122 | }, 3000); 123 | }} 124 | /> 125 | 126 | Show my online status 127 | 128 | 129 | 130 | 131 | {/* Section: Other Settings */} 132 | 133 | 134 | 135 | 136 | Dark Interface 137 | 138 | 139 | 140 | Choose your prefered language 141 | 142 | 143 | 144 | { 150 | setLanguage(selectedLocale[0]); 151 | }} 152 | > 153 | {router.locales?.map((locale) => { 154 | let text = ""; 155 | 156 | switch (locale) { 157 | case "en": 158 | text = "English"; 159 | break; 160 | case "de": 161 | text = "Deutsch"; 162 | } 163 | 164 | return ( 165 | setSelectedLocale([locale])} 171 | > 172 | {text} 173 | 174 | ); 175 | })} 176 | 177 | 178 | 179 | 180 | 181 | {/* Section: Delete my Account */} 182 | 183 | 191 | Caution! This can´t be undone! 192 | 193 | 194 | 195 | 196 | 197 | Please delete your account only if you really don't plan 198 | to come back. Your data will be deleted completely and 199 | irrevocably! It was nice having you here. See you around. 200 | 201 | 202 | 203 | 204 | {/* */} 205 | } 208 | isOpen={isDeletePopoverOpen} 209 | setIsOpen={setIsDeletePopoverOpen} 210 | width="25rem" 211 | > 212 | 213 | Confirm 214 | 215 | 216 | 217 | Please delete your account only if you really don't 218 | plan to come back. Your data will be deleted completely 219 | and irrevocably! It was nice having you here. See you 220 | around. 221 | 222 | 223 | 228 | 237 | 250 | 251 | 252 | 253 | 254 | 255 | )} 256 | 257 | {/* For security: Not show content if not logged in */} 258 | {!isLoggedIn && ( 259 | 260 | You are not logged in! 261 | 262 | )} 263 | 264 | 265 | ); 266 | } 267 | 268 | // Export serverside props 269 | export async function getServerSideProps(context: GetServerSidePropsContext) { 270 | // Get the language cookie 271 | const langCookie = getCookie("NEXT_LOCALE", context.res); 272 | 273 | // Init PocketBase 274 | const pb = await initPocketBase(context); 275 | 276 | // Strip the authData from the pb authStore 277 | const authData = JSON.parse(JSON.stringify(pb.authStore)); 278 | 279 | let userRoles; 280 | let roles; 281 | 282 | // If the user is not logged in, redirect to the login page 283 | if (!pb.authStore.isValid) { 284 | return { 285 | redirect: { 286 | destination: "/login", 287 | permanent: false, 288 | }, 289 | }; 290 | } 291 | 292 | // If the language cookie is not the actual language, redirect to correct language 293 | if (langCookie && langCookie !== context.locale) { 294 | return { 295 | redirect: { 296 | destination: "/" + langCookie + context.resolvedUrl, 297 | permanent: false, 298 | }, 299 | }; 300 | } 301 | 302 | // Get the user roles 303 | await pb 304 | .collection("users") 305 | .getOne(authData.baseModel.id, { 306 | expand: "roles", 307 | }) 308 | .then((res) => { 309 | userRoles = res.expand.roles; 310 | roles = userRoles.map((role: { name: string }) => role.name); 311 | }) 312 | .catch(() => { 313 | roles = []; 314 | }); 315 | 316 | // Return the props if user is logged in and correct language is set 317 | return { 318 | props: { 319 | isLoggedIn: pb.authStore.isValid, 320 | authData: authData.baseModel, 321 | roles: roles, 322 | }, 323 | }; 324 | } 325 | 326 | DashboardSettings.getLayout = function getLayout(page: ReactElement) { 327 | return ( 328 | 329 | {page} 330 | 331 | ); 332 | }; 333 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | //Import components 2 | import Head from "next/head"; 3 | import { Card, Container, Grid, Text, useTheme } from "@nextui-org/react"; 4 | import useTranslation from "next-translate/useTranslation"; 5 | import { getCookie } from "cookies-next"; 6 | import Layout from "../components/layouts/RootLayout"; 7 | import initPocketBase from "../helpers/initPocketbase"; 8 | import LanguageSwitcher from "../components/interactive/LanguageSwitcher"; 9 | 10 | // Import icons 11 | import { SiTailwindcss } from "react-icons/si"; 12 | import { AiOutlineApi, AiOutlineHtml5 } from "react-icons/ai"; 13 | import { FaReact, FaWpforms } from "react-icons/fa"; 14 | import { MdOutlineDarkMode } from "react-icons/md"; 15 | import { BsTranslate } from "react-icons/bs"; 16 | 17 | // Import types 18 | import type { ReactElement } from "react"; 19 | import type { GetServerSidePropsContext } from "next"; 20 | 21 | // Fire the Home page 22 | export default function Home() { 23 | const { t } = useTranslation("common"); 24 | 25 | const MockItems = ({ 26 | text, 27 | icon, 28 | title, 29 | }: { 30 | text: string; 31 | icon: React.ReactNode; 32 | title: string; 33 | }) => { 34 | return ( 35 | 36 | 37 | 38 | {icon} 39 | 40 | 48 | {title} 49 | 50 | 51 | 52 | 53 | 54 | {text} 55 | 56 | 57 | ); 58 | }; 59 | 60 | const { theme } = useTheme(); 61 | 62 | return ( 63 | <> 64 | 65 | {t("sitename")} 66 | 67 | 68 | 69 | 73 | {t("title")} 74 | 75 | 76 | 77 | 78 | 79 | 90 | } 91 | title="Next.js" 92 | /> 93 | 94 | 95 | 106 | } 107 | title="Pocketpase" 108 | /> 109 | 110 | 111 | 122 | } 123 | title="Tailwind & NextUI" 124 | /> 125 | 126 | 127 | 138 | } 139 | title="React-Icons" 140 | /> 141 | 142 | 143 | 154 | } 155 | title="Dark Mode" 156 | /> 157 | 158 | 159 | 170 | } 171 | title="Translation" 172 | /> 173 | 174 | 175 | 186 | } 187 | title="Formik - Coming Soon" 188 | /> 189 | 190 | 191 | 192 | 193 | 194 | ); 195 | } 196 | 197 | // Export serverside props 198 | export async function getServerSideProps(context: GetServerSidePropsContext) { 199 | // Get the language cookie 200 | const langCookie = getCookie("NEXT_LOCALE", context.res); 201 | 202 | // Init PocketBase 203 | const pb = await initPocketBase(context); 204 | 205 | // Strip the authData from the pb authStore 206 | const authData = JSON.parse(JSON.stringify(pb.authStore)); 207 | 208 | // If the language cookie is not the actual language, redirect to correct language 209 | if (langCookie && langCookie !== context.locale) { 210 | return { 211 | redirect: { 212 | destination: "/" + langCookie + context.resolvedUrl, 213 | permanent: false, 214 | }, 215 | }; 216 | } 217 | 218 | // Return the props if the correct language is set 219 | return { 220 | props: { 221 | isLoggedIn: pb.authStore.isValid, 222 | authData: authData.baseModel, 223 | }, 224 | }; 225 | } 226 | 227 | Home.getLayout = function getLayout(page: ReactElement) { 228 | return {page}; 229 | }; 230 | -------------------------------------------------------------------------------- /pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | // Import components 2 | import { useEffect, useState } from "react"; 3 | import { useRouter } from "next/router"; 4 | import Head from "next/head"; 5 | import Link from "next/link"; 6 | import PocketBase, { type AuthProviderInfo } from "pocketbase"; 7 | import initPocketBase from "../../helpers/initPocketbase"; 8 | import Layout from "../../components/layouts/RootLayout"; 9 | import useTranslation from "next-translate/useTranslation"; 10 | import { setCookie, getCookie, deleteCookie } from "cookies-next"; 11 | import { Button, Container } from "@nextui-org/react"; 12 | 13 | // Import types 14 | import type { ReactElement } from "react"; 15 | import type { GetServerSidePropsContext } from "next"; 16 | import type { authData } from "../../types/user"; 17 | 18 | // Import icons 19 | import { FaTwitch, FaDiscord } from "react-icons/fa"; 20 | 21 | // Fire the site 22 | export default function LoginPage({ 23 | isLoggedIn, 24 | authData, 25 | }: { 26 | isLoggedIn: boolean; 27 | authData: authData; 28 | }) { 29 | const { t } = useTranslation("common"); 30 | const router = useRouter(); 31 | const [allProvider, setAllProvider] = useState(); 32 | 33 | // Fire login logic on mount 34 | useEffect(() => { 35 | // Init PocketBase 36 | const pb = new PocketBase(process.env.NEXT_PUBLIC_API_URL); 37 | 38 | // Get the saved provider data from localstorage 39 | (async () => { 40 | const fromStorage = JSON.parse(localStorage.getItem("provider") || "{}"); 41 | 42 | // Fires when the user comes back from the oauth2 provider 43 | if (router.query.code) { 44 | const code = router.query.code.toString(); 45 | 46 | // Login the user 47 | await pb 48 | .collection("users") 49 | .authWithOAuth2( 50 | fromStorage.name, 51 | code, 52 | fromStorage.codeVerifier, 53 | process.env.NEXT_PUBLIC_SITE_URL + "/login", 54 | { 55 | banned: false, 56 | showOnline: true, 57 | lastActive: new Date(), 58 | emailVisibility: false, 59 | verified: true, 60 | } 61 | ) 62 | .then((response) => { 63 | // Get the authStore from localstorage 64 | const authStoreLocalStorage = 65 | localStorage.getItem("pocketbase_auth"); 66 | 67 | // Update the user data with the data from the oauth2 provider 68 | const dataToAdd = { 69 | avatarUrl: response.meta?.avatarUrl, 70 | lastActive: new Date(), 71 | }; 72 | pb.collection("users").update(response.record.id, dataToAdd); 73 | 74 | // Remove the provider data from localstorage 75 | localStorage.removeItem("provider"); 76 | 77 | if (authStoreLocalStorage) { 78 | // Set the authStore cookie 79 | setCookie("pb_auth", authStoreLocalStorage, { 80 | maxAge: 60 * 60 * 24 * 14, 81 | path: "/", 82 | secure: true, 83 | sameSite: "strict", 84 | }); 85 | 86 | //Remove the authStore from localstorage 87 | localStorage.removeItem("pocketbase_auth"); 88 | 89 | // Refresh page to login the user with the new cookie 90 | router.push("/login", "/login", { 91 | locale: router.locale, 92 | }); 93 | } 94 | }) 95 | // BACKLOG: Add error handling 96 | .catch((error) => { 97 | console.error(error); 98 | }); 99 | 100 | // When the user is not logged in and it is not a redirect from the oauth2 provider 101 | } else if (!router.query.code && !isLoggedIn) { 102 | // Get the enabled oauth2 providers 103 | (async () => { 104 | const authMethods = await pb.collection("users").listAuthMethods(); 105 | setAllProvider(authMethods.authProviders); 106 | })(); 107 | } 108 | })(); 109 | 110 | // eslint-disable-next-line react-hooks/exhaustive-deps 111 | }, []); 112 | 113 | return ( 114 | <> 115 | 116 | {t("sitename") + " | Login"} 117 | 118 | 119 | 120 | {!router.query.code ?

Login

:

Wait for redirect...

} 121 | {allProvider && !isLoggedIn && ( 122 |
123 | {allProvider.map((provider) => ( 124 | 146 | ))} 147 |
148 | )} 149 |
150 | 151 | ); 152 | } 153 | 154 | // Export serverside props 155 | export async function getServerSideProps(context: GetServerSidePropsContext) { 156 | // Get language cookie for correct redirect path 157 | const languageCookie = getCookie("NEXT_LOCALE", context); 158 | 159 | // Get the redirect after login cookie 160 | const redirectAfterLoginCookie = getCookie("redirect_after_login", context); 161 | 162 | // Init PocketBase 163 | const pb = await initPocketBase(context); 164 | 165 | // Strip the authData from the pb authStore 166 | const authData = JSON.parse(JSON.stringify(pb.authStore)); 167 | 168 | // Redirect if the user is logged in 169 | if (pb.authStore.isValid) { 170 | // Remove the redirect after login cookie 171 | deleteCookie("redirect_after_login", context); 172 | 173 | // Handle the redirect string 174 | const redirect = redirectAfterLoginCookie ? redirectAfterLoginCookie : "/"; 175 | const language = languageCookie ? languageCookie : "en"; 176 | 177 | return { 178 | redirect: { 179 | destination: "/" + language + redirect, 180 | permanent: false, 181 | }, 182 | }; 183 | } 184 | 185 | // If the language cookie is not the actual language, redirect to correct language 186 | if (languageCookie && languageCookie !== context.locale) { 187 | return { 188 | redirect: { 189 | destination: "/" + languageCookie + context.resolvedUrl, 190 | permanent: false, 191 | }, 192 | }; 193 | } 194 | 195 | // Return the props when user is not logged in and correct language is set 196 | return { 197 | props: { 198 | isLoggedIn: pb.authStore.isValid, 199 | authData: authData.baseModel, 200 | }, 201 | }; 202 | } 203 | 204 | // Load the layout 205 | LoginPage.getLayout = function getLayout(page: ReactElement) { 206 | return {page}; 207 | }; 208 | -------------------------------------------------------------------------------- /pages/user/[slug]/edit/index.tsx: -------------------------------------------------------------------------------- 1 | //Import components 2 | import Head from "next/head"; 3 | import { Container, Text } from "@nextui-org/react"; 4 | import useTranslation from "next-translate/useTranslation"; 5 | import { getCookie } from "cookies-next"; 6 | import Layout from "../../../../components/layouts/RootLayout"; 7 | import initPocketBase from "../../../../helpers/initPocketbase"; 8 | 9 | // Import types 10 | import type { ReactElement } from "react"; 11 | import type { GetServerSidePropsContext } from "next"; 12 | 13 | // Fire the Home page 14 | export default function UserEdit() { 15 | const { t } = useTranslation("common"); 16 | 17 | return ( 18 | <> 19 | 20 | {t("sitename") + " | Edit User"} 21 | 22 | 23 | 24 | Edit User 25 | 26 | Content 27 | 28 | ); 29 | } 30 | 31 | // Export serverside props 32 | export async function getServerSideProps(context: GetServerSidePropsContext) { 33 | // Get the language cookie 34 | const langCookie = getCookie("NEXT_LOCALE", context.res); 35 | 36 | // Init PocketBase 37 | const pb = await initPocketBase(context); 38 | 39 | // Strip the authData from the pb authStore 40 | const authData = JSON.parse(JSON.stringify(pb.authStore)); 41 | 42 | // If the language cookie is not the actual language, redirect to correct language 43 | if (langCookie && langCookie !== context.locale) { 44 | return { 45 | redirect: { 46 | destination: "/" + langCookie + context.resolvedUrl, 47 | permanent: false, 48 | }, 49 | }; 50 | } 51 | 52 | // Return the props if the correct language is set 53 | return { 54 | props: { 55 | isLoggedIn: pb.authStore.isValid, 56 | authData: authData.baseModel, 57 | }, 58 | }; 59 | } 60 | 61 | UserEdit.getLayout = function getLayout(page: ReactElement) { 62 | return {page}; 63 | }; 64 | -------------------------------------------------------------------------------- /pages/user/[slug]/index.tsx: -------------------------------------------------------------------------------- 1 | //Import components 2 | import Head from "next/head"; 3 | import { Container, Text } from "@nextui-org/react"; 4 | import useTranslation from "next-translate/useTranslation"; 5 | import { getCookie } from "cookies-next"; 6 | import Layout from "../../../components/layouts/RootLayout"; 7 | import initPocketBase from "../../../helpers/initPocketbase"; 8 | 9 | // Import types 10 | import type { ReactElement } from "react"; 11 | import type { GetServerSidePropsContext } from "next"; 12 | 13 | // Fire the Home page 14 | export default function UserProfile() { 15 | const { t } = useTranslation("common"); 16 | 17 | return ( 18 | <> 19 | 20 | {t("sitename") + " | User Profile"} 21 | 22 | 23 | 24 | User Profile 25 | 26 | Content 27 | 28 | ); 29 | } 30 | 31 | // Export serverside props 32 | export async function getServerSideProps(context: GetServerSidePropsContext) { 33 | // Get the language cookie 34 | const langCookie = getCookie("NEXT_LOCALE", context.res); 35 | 36 | // Init PocketBase 37 | const pb = await initPocketBase(context); 38 | 39 | // Strip the authData from the pb authStore 40 | const authData = JSON.parse(JSON.stringify(pb.authStore)); 41 | 42 | // If the language cookie is not the actual language, redirect to correct language 43 | if (langCookie && langCookie !== context.locale) { 44 | return { 45 | redirect: { 46 | destination: "/" + langCookie + context.resolvedUrl, 47 | permanent: false, 48 | }, 49 | }; 50 | } 51 | 52 | // Return the props if the correct language is set 53 | return { 54 | props: { 55 | isLoggedIn: pb.authStore.isValid, 56 | authData: authData.baseModel, 57 | }, 58 | }; 59 | } 60 | 61 | UserProfile.getLayout = function getLayout(page: ReactElement) { 62 | return {page}; 63 | }; 64 | -------------------------------------------------------------------------------- /pages/user/index.tsx: -------------------------------------------------------------------------------- 1 | //Import components 2 | import Head from "next/head"; 3 | import { Container, Text } from "@nextui-org/react"; 4 | import useTranslation from "next-translate/useTranslation"; 5 | import Layout from "../../components/layouts/RootLayout"; 6 | import initPocketBase from "../../helpers/initPocketbase"; 7 | import { getCookie } from "cookies-next"; 8 | 9 | // Import types 10 | import type { ReactElement } from "react"; 11 | import type { GetServerSidePropsContext } from "next"; 12 | 13 | // Fire the Home page 14 | export default function User() { 15 | const { t } = useTranslation("common"); 16 | 17 | return ( 18 | <> 19 | 20 | {t("sitename") + " | User"} 21 | 22 | 23 | 24 | User 25 | 26 | Content 27 | 28 | ); 29 | } 30 | 31 | // Export serverside props 32 | export async function getServerSideProps(context: GetServerSidePropsContext) { 33 | // Get the language cookie 34 | const langCookie = getCookie("NEXT_LOCALE", context.res); 35 | 36 | // Init PocketBase 37 | const pb = await initPocketBase(context); 38 | 39 | // Strip the authData from the pb authStore 40 | const authData = JSON.parse(JSON.stringify(pb.authStore)); 41 | 42 | // If the language cookie is not the actual language, redirect to correct language 43 | if (langCookie && langCookie !== context.locale) { 44 | return { 45 | redirect: { 46 | destination: "/" + langCookie + context.resolvedUrl, 47 | permanent: false, 48 | }, 49 | }; 50 | } 51 | 52 | // Return the props if the correct language is set 53 | return { 54 | props: { 55 | isLoggedIn: pb.authStore.isValid, 56 | authData: authData.baseModel, 57 | }, 58 | }; 59 | } 60 | 61 | User.getLayout = function getLayout(page: ReactElement) { 62 | return {page}; 63 | }; 64 | -------------------------------------------------------------------------------- /pages/user/me/edit/index.tsx: -------------------------------------------------------------------------------- 1 | //Import components 2 | import Head from "next/head"; 3 | import { Container, Text } from "@nextui-org/react"; 4 | import useTranslation from "next-translate/useTranslation"; 5 | import Layout from "../../../../components/layouts/RootLayout"; 6 | import initPocketBase from "../../../../helpers/initPocketbase"; 7 | import { getCookie } from "cookies-next"; 8 | 9 | // Import types 10 | import type { ReactElement } from "react"; 11 | import type { GetServerSidePropsContext } from "next"; 12 | 13 | // Fire the Home page 14 | export default function MeEdit() { 15 | const { t } = useTranslation("common"); 16 | 17 | return ( 18 | <> 19 | 20 | {t("sitename") + " | Edit my profile"} 21 | 22 | 23 | 24 | Edit my profile 25 | 26 | Content 27 | 28 | ); 29 | } 30 | 31 | // Export serverside props 32 | export async function getServerSideProps(context: GetServerSidePropsContext) { 33 | // Get the language cookie 34 | const langCookie = getCookie("NEXT_LOCALE", context.res); 35 | 36 | // Init PocketBase 37 | const pb = await initPocketBase(context); 38 | 39 | // Strip the authData from the pb authStore 40 | const authData = JSON.parse(JSON.stringify(pb.authStore)); 41 | 42 | // If the language cookie is not the actual language, redirect to correct language 43 | if (langCookie && langCookie !== context.locale) { 44 | return { 45 | redirect: { 46 | destination: "/" + langCookie + context.resolvedUrl, 47 | permanent: false, 48 | }, 49 | }; 50 | } 51 | 52 | // Return the props if the correct language is set 53 | return { 54 | props: { 55 | isLoggedIn: pb.authStore.isValid, 56 | authData: authData.baseModel, 57 | }, 58 | }; 59 | } 60 | 61 | MeEdit.getLayout = function getLayout(page: ReactElement) { 62 | return {page}; 63 | }; 64 | -------------------------------------------------------------------------------- /pages/user/me/index.tsx: -------------------------------------------------------------------------------- 1 | //Import components 2 | import Head from "next/head"; 3 | import { Container, Text } from "@nextui-org/react"; 4 | import useTranslation from "next-translate/useTranslation"; 5 | import { getCookie } from "cookies-next"; 6 | import Layout from "../../../components/layouts/RootLayout"; 7 | import initPocketBase from "../../../helpers/initPocketbase"; 8 | 9 | // Import types 10 | import type { ReactElement } from "react"; 11 | import type { GetServerSidePropsContext } from "next"; 12 | 13 | // Fire the Home page 14 | export default function MyProfile() { 15 | const { t } = useTranslation("common"); 16 | 17 | return ( 18 | <> 19 | 20 | {t("sitename") + " | My Profile"} 21 | 22 | 23 | 24 | My Profile 25 | 26 | Content 27 | 28 | ); 29 | } 30 | 31 | // Export serverside props 32 | export async function getServerSideProps(context: GetServerSidePropsContext) { 33 | // Get the language cookie 34 | const langCookie = getCookie("NEXT_LOCALE", context.res); 35 | 36 | // Init PocketBase 37 | const pb = await initPocketBase(context); 38 | 39 | // Strip the authData from the pb authStore 40 | const authData = JSON.parse(JSON.stringify(pb.authStore)); 41 | 42 | // If the language cookie is not the actual language, redirect to correct language 43 | if (langCookie && langCookie !== context.locale) { 44 | return { 45 | redirect: { 46 | destination: "/" + langCookie + context.resolvedUrl, 47 | permanent: false, 48 | }, 49 | }; 50 | } 51 | 52 | // Return the props if the correct language is set 53 | return { 54 | props: { 55 | isLoggedIn: pb.authStore.isValid, 56 | authData: authData.baseModel, 57 | }, 58 | }; 59 | } 60 | 61 | MyProfile.getLayout = function getLayout(page: ReactElement) { 62 | return {page}; 63 | }; 64 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristianSeelemann/nextjs-with-pocketbase/455e145f295c7cfed384257d8dfab12c2a954751/public/favicon.ico -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | @tailwind utilities; 3 | 4 | /* ===== Scrollbar CSS ===== */ 5 | /* Firefox */ 6 | * { 7 | scrollbar-width: auto; 8 | scrollbar-color: #abc8ff2a transparent; 9 | } 10 | 11 | body { 12 | overflow-y: scroll !important; 13 | } 14 | 15 | html.dark-theme * { 16 | scrollbar-color: #abc8ff2a transparent; 17 | } 18 | 19 | /* Chrome, Edge, and Safari */ 20 | *::-webkit-scrollbar { 21 | width: 0.4rem; 22 | } 23 | 24 | *::-webkit-scrollbar-track { 25 | background: transparent; 26 | } 27 | 28 | *::-webkit-scrollbar-thumb { 29 | background-color: #3d41494d; 30 | border-radius: 12px; 31 | } 32 | 33 | html.dark-theme *::-webkit-scrollbar-thumb { 34 | background-color: #97a1b44d; 35 | border-radius: 12px; 36 | } 37 | 38 | .nextui-navbar-collapse-wrapper { 39 | overflow: hidden; 40 | } 41 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | primary: ["Bai Jamjuree", "sans-serif"], 11 | secondary: ["Chakra Petch", "sans-serif"], 12 | }, 13 | }, 14 | screens: { 15 | xs: "650px", 16 | sm: "960px", 17 | md: "1280px", 18 | lg: "1400px", 19 | xl: "1920px", 20 | }, 21 | }, 22 | plugins: [], 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /types/user.ts: -------------------------------------------------------------------------------- 1 | export interface authData { 2 | id: string; 3 | username: string; 4 | name: string | null; 5 | avatar: string | null; 6 | avatarUrl: string | null; 7 | banned: boolean; 8 | bannedUntil: Date | null; 9 | bannedReason: string | null; 10 | collectionId: string; 11 | collectionName: string; 12 | created: string; 13 | email: string | null; 14 | emailVisibility: boolean; 15 | firstName: string | null; 16 | lastName: string | null; 17 | lastActive: Date; 18 | roles: string[] | null; 19 | showOnline: boolean; 20 | updated: string; 21 | verified: boolean; 22 | } 23 | --------------------------------------------------------------------------------