├── .env.local.example
├── .gitignore
├── README.md
├── TODOs.md
├── components
├── BMAC.js
├── Footer.js
├── Hero.js
├── HiddenEmail.js
├── LoginButtons.js
├── MDXComponents.js
├── NavBar.js
├── NextBreadcrumb.js
├── PageShell.js
└── icons.js
├── firestore.rules
├── jsconfig.json
├── lib
├── auth.js
├── db-admin.js
├── db.js
├── firebase-admin.js
├── firebase.js
└── middlewares.js
├── next.config.js
├── package.json
├── pages
├── _app.js
├── _document.js
├── account.js
├── dashboard.js
├── faq.mdx
├── index.js
└── test.js
├── public
├── favicon.ico
└── vercel.svg
├── styles
└── theme.js
├── utils
├── fetcher.js
└── logger.js
└── yarn.lock
/.env.local.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_FIREBASE_API_KEY =
2 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN =
3 | NEXT_PUBLIC_FIREBASE_PROJECT_ID =
4 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET =
5 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID =
6 | NEXT_PUBLIC_FIREBASE_APP_ID =
7 |
8 | FIREBASE_PRIVATE_KEY =
9 | FIREBASE_CLIENT_EMAIL =
10 |
11 | NEXT_PUBLIC_LOGFLARE_KEY =
12 | NEXT_PUBLIC_LOGFLARE_STREAM =
13 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Starter Template for Next.js with MDX, Firebase, Chakra UI, Logflare and Stripe
2 |
3 | ---
4 |
5 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
6 |
7 | ## Getting Started
8 |
9 | First, run the development server:
10 |
11 | ```bash
12 | npm run dev
13 | # or
14 | yarn dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
20 |
21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
22 |
23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
24 |
25 | ## Learn More
26 |
27 | To learn more about Next.js, take a look at the following resources:
28 |
29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
31 |
32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
33 |
34 | ## Deploy on Vercel
35 |
36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
37 |
38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
39 |
--------------------------------------------------------------------------------
/TODOs.md:
--------------------------------------------------------------------------------
1 | - [ ] refactor next-seo in \_app.js
2 | - [ ] clean up \_document.js
3 | - [ ] add a fuller mdx example
4 | - [ ] add stripe integration
5 | - [ ] handle multiple login provider merging
6 | - [ ] add example pages of a few different types
7 |
--------------------------------------------------------------------------------
/components/BMAC.js:
--------------------------------------------------------------------------------
1 | const BMAC = () => {
2 | return (
3 | <>
4 |
5 |
10 |
11 |
24 | >
25 | );
26 | };
27 |
28 | export default BMAC;
29 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NextLink from 'next/link';
3 | import { Link, Flex, HStack } from '@chakra-ui/react';
4 |
5 | const FooterLink = ({ href, children }) => {
6 | return (
7 |
8 |
9 | {children}
10 |
11 |
12 | );
13 | };
14 |
15 | const Footer = () => {
16 | return (
17 |
18 | Privacy
19 | Terms
20 | FAQ
21 | Home
22 |
23 | );
24 | };
25 |
26 | export default Footer;
27 |
--------------------------------------------------------------------------------
/components/Hero.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NextLink from 'next/link';
3 | import {
4 | Box,
5 | Button,
6 | Avatar,
7 | HStack,
8 | Flex,
9 | Link,
10 | Icon,
11 | SkeletonCircle,
12 | Heading,
13 | Text,
14 | } from '@chakra-ui/react';
15 | import LoginButtons from '@/components/LoginButtons';
16 |
17 | const Hero = () => {
18 | return (
19 |
26 |
27 |
28 |
29 | Here are some{' '}
30 |
31 |
37 | important stuff
38 |
39 |
40 | {' '}
41 | about this website.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default Hero;
53 |
--------------------------------------------------------------------------------
/components/HiddenEmail.js:
--------------------------------------------------------------------------------
1 | export default function HiddenEmail() {
2 | const clickHander = (e) => {
3 | window.location.href =
4 | 'mailto:' +
5 | e.target.dataset.name +
6 | '@' +
7 | e.target.dataset.domain +
8 | '.' +
9 | e.target.dataset.tld;
10 | return false;
11 | };
12 | return (
13 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/LoginButtons.js:
--------------------------------------------------------------------------------
1 | import { Button, Flex } from '@chakra-ui/react';
2 |
3 | import { useAuth } from '@/lib/auth';
4 | import { FcGoogle } from 'react-icons/fc';
5 | import { FaTwitter } from 'react-icons/fa';
6 |
7 | const LoginButtons = () => {
8 | const auth = useAuth();
9 |
10 | return (
11 |
12 | {/* */}
24 |
35 |
36 | );
37 | };
38 |
39 | export default LoginButtons;
40 |
--------------------------------------------------------------------------------
/components/MDXComponents.js:
--------------------------------------------------------------------------------
1 | import NextLink from 'next/link';
2 | import { Link } from '@chakra-ui/react';
3 | import Image from 'next/image';
4 | import { Box, Code, Heading } from '@chakra-ui/react';
5 |
6 | const CustomLink = (props) => {
7 | const href = props.href;
8 | const isInternalLink = href && (href.startsWith('/') || href.startsWith('#'));
9 |
10 | if (isInternalLink) {
11 | return (
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | return ;
19 | };
20 |
21 | const MDXComponents = {
22 | Image,
23 | a: CustomLink,
24 | pre: (props) =>
,
25 | // code: CodeBlock,
26 | inlineCode: Code,
27 | h1: (props) => ,
28 | h2: (props) => ,
29 | h3: (props) => ,
30 | p: (props) => ,
31 | };
32 |
33 | export default MDXComponents;
34 |
--------------------------------------------------------------------------------
/components/NavBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NextLink from 'next/link';
3 | import Router from 'next/router';
4 | import { Logo } from './icons';
5 | import {
6 | Box,
7 | Button,
8 | Heading,
9 | Avatar,
10 | HStack,
11 | Flex,
12 | Link,
13 | Icon,
14 | SkeletonCircle,
15 | useMediaQuery,
16 | } from '@chakra-ui/react';
17 |
18 | import { useAuth } from '@/lib/auth';
19 |
20 | const WrappedLink = ({ href, children }) => {
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | const Navbar = (props) => {
29 | const { user } = useAuth();
30 | const innerWidth = props.innerWidth || '1040px';
31 | const [largeScreen] = useMediaQuery('(min-width: 700px)');
32 |
33 | return (
34 |
43 |
52 |
53 |
54 |
55 |
56 | {largeScreen ? (
57 |
58 |
59 | Site Name
60 |
61 |
62 | ) : null}
63 |
64 |
65 |
66 | FAQ
67 |
68 | {user ? (
69 | <>
70 |
77 |
78 |
79 |
80 | >
81 | ) : null}
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default Navbar;
89 |
--------------------------------------------------------------------------------
/components/NextBreadcrumb.js:
--------------------------------------------------------------------------------
1 | import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react';
2 | import NextLink from 'next/link';
3 |
4 | const NextBreadcrumb = ({ pagePath, pageName }) => {
5 | return (
6 |
7 |
8 |
9 | Home
10 |
11 |
12 |
13 |
14 | {pageName}
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default NextBreadcrumb;
22 |
--------------------------------------------------------------------------------
/components/PageShell.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NextLink from 'next/link';
3 | import { Box, Button, Flex, Link, Avatar, Icon } from '@chakra-ui/react';
4 |
5 | import Footer from './Footer';
6 | import Navbar from './NavBar';
7 |
8 | const inner = {
9 | m: '0 auto',
10 | maxW: '1040px',
11 | width: '100%',
12 | flexGrow: 1,
13 | };
14 |
15 | const DashboardShell = ({ children }) => {
16 | return (
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default DashboardShell;
28 |
--------------------------------------------------------------------------------
/components/icons.js:
--------------------------------------------------------------------------------
1 | import { createIcon } from '@chakra-ui/icons';
2 |
3 | export const Logo = createIcon({
4 | displayName: 'logo',
5 | viewBox: '0 0 30 30',
6 | path: (
7 |
12 | ),
13 | });
14 |
--------------------------------------------------------------------------------
/firestore.rules:
--------------------------------------------------------------------------------
1 | rules_version = '2';
2 | service cloud.firestore {
3 | match /databases/{database}/documents {
4 | match /users/{uid} {
5 | allow read, write: if isUser(uid);
6 | }
7 | }
8 | }
9 | function isUser(uid) {
10 | return isSignedIn() && request.auth.uid == uid;
11 | }
12 | function isSignedIn() {
13 | return request.auth.uid != null;
14 | }
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/components/*": ["components/*"],
6 | "@/lib/*": ["lib/*"],
7 | "@/utils/*": ["utils/*"],
8 | "@/models/*": ["models/*"],
9 | "@/styles/*": ["styles/*"]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/lib/auth.js:
--------------------------------------------------------------------------------
1 | //Ref https://docs.react2025.com/firebase/use-auth
2 | import React, { useState, useEffect, useContext, createContext } from 'react';
3 | import Router from 'next/router';
4 | import firebase from './firebase';
5 | import { createUser } from './db';
6 |
7 | const authContext = createContext();
8 |
9 | export function AuthProvider({ children }) {
10 | const auth = useFirebaseAuth();
11 | return {children};
12 | }
13 |
14 | export const useAuth = () => {
15 | return useContext(authContext);
16 | };
17 |
18 | function useFirebaseAuth() {
19 | const [user, setUser] = useState(null);
20 | const [loading, setLoading] = useState(true);
21 |
22 | const handleUser = async (rawUser) => {
23 | console.log('handleUser called', new Date());
24 | if (rawUser) {
25 | const user = await formatUser(rawUser);
26 | const { token, ...userWithoutToken } = user;
27 |
28 | createUser(user.uid, userWithoutToken);
29 | setUser(user);
30 |
31 | setLoading(false);
32 | return user;
33 | } else {
34 | setUser(false);
35 | setLoading(false);
36 | return false;
37 | }
38 | };
39 |
40 | const signinWithEmail = (email, password, redirect) => {
41 | setLoading(true);
42 | return firebase
43 | .auth()
44 | .signInWithEmailAndPassword(email, password)
45 | .then((response) => {
46 | handleUser(response.user);
47 |
48 | if (redirect) {
49 | Router.push(redirect);
50 | }
51 | });
52 | };
53 |
54 | const signinWithGitHub = (redirect) => {
55 | setLoading(true);
56 | return firebase
57 | .auth()
58 | .signInWithPopup(new firebase.auth.GithubAuthProvider())
59 | .then((response) => {
60 | handleUser(response.user);
61 |
62 | if (redirect) {
63 | Router.push(redirect);
64 | }
65 | });
66 | };
67 |
68 | const signinWithTwitter = (redirect) => {
69 | setLoading(true);
70 | return firebase
71 | .auth()
72 | .signInWithPopup(new firebase.auth.TwitterAuthProvider())
73 | .then((response) => {
74 | handleUser(response.user);
75 |
76 | if (redirect) {
77 | Router.push(redirect);
78 | }
79 | });
80 | };
81 |
82 | const signinWithGoogle = (redirect) => {
83 | setLoading(true);
84 | return firebase
85 | .auth()
86 | .signInWithPopup(new firebase.auth.GoogleAuthProvider())
87 | .then((response) => {
88 | handleUser(response.user);
89 |
90 | if (redirect) {
91 | Router.push(redirect);
92 | }
93 | });
94 | };
95 |
96 | const signout = () => {
97 | return firebase
98 | .auth()
99 | .signOut()
100 | .then(() => handleUser(false));
101 | };
102 |
103 | useEffect(() => {
104 | const unsubscribe = firebase.auth().onIdTokenChanged(handleUser);
105 | return () => unsubscribe();
106 | }, []);
107 |
108 | // useEffect(() => {
109 | // const interval = setInterval(async () => {
110 | // if (user) {
111 | // const token = await firebase
112 | // .auth()
113 | // .currentUser.getIdToken(/* forceRefresh */ true);
114 | // setUser(user);
115 | // console.log('refreshed token');
116 | // }
117 | // }, 30 * 60 * 1000 /*every 30min, assuming token expires every 1hr*/);
118 | // return () => clearInterval(interval);
119 | // }, [user]); // needs to depend on user to have closure on a valid user object in callback fun
120 |
121 | const getFreshToken = async () => {
122 | console.log('getFreshToken called', new Date());
123 | const currentUser = firebase.auth().currentUser;
124 | if (currentUser) {
125 | const token = await currentUser.getIdToken(false);
126 | return `${token}`;
127 | } else {
128 | return '';
129 | }
130 | };
131 |
132 | return {
133 | user,
134 | loading,
135 | signinWithEmail,
136 | signinWithGitHub,
137 | signinWithTwitter,
138 | signinWithGoogle,
139 | signout,
140 | getFreshToken,
141 | };
142 | }
143 |
144 | // const getStripeRole = async () => {
145 | // await firebase.auth().currentUser.getIdToken(true);
146 | // const decodedToken = await firebase.auth().currentUser.getIdTokenResult();
147 | // return decodedToken.claims.stripeRole || 'free';
148 | // };
149 |
150 | const formatUser = async (user) => {
151 | // const token = await user.getIdToken(/* forceRefresh */ true);
152 | const decodedToken = await user.getIdTokenResult(/*forceRefresh*/ true);
153 | const { token, expirationTime } = decodedToken;
154 | console.log(token);
155 | return {
156 | uid: user.uid,
157 | email: user.email,
158 | name: user.displayName,
159 | provider: user.providerData[0].providerId,
160 | photoUrl: user.photoURL,
161 | token,
162 | expirationTime,
163 | // stripeRole: await getStripeRole(),
164 | };
165 | };
166 |
--------------------------------------------------------------------------------
/lib/db-admin.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dingran/nextjs-starter-template/34760d77671d21a6e514c994bf223cbca96b9760/lib/db-admin.js
--------------------------------------------------------------------------------
/lib/db.js:
--------------------------------------------------------------------------------
1 | import firebase from './firebase';
2 | const firestore = firebase.firestore();
3 |
4 | export async function createUser(uid, data) {
5 | return await firestore
6 | .collection('users')
7 | .doc(uid)
8 | .set({ uid, ...data }, { merge: true });
9 | }
10 |
--------------------------------------------------------------------------------
/lib/firebase-admin.js:
--------------------------------------------------------------------------------
1 | import * as admin from 'firebase-admin';
2 |
3 | if (!admin.apps.length) {
4 | admin.initializeApp({
5 | credential: admin.credential.cert({
6 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
7 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
8 | privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
9 | }),
10 | });
11 | }
12 |
13 | const firestore = admin.firestore();
14 | const auth = admin.auth();
15 |
16 | export { firestore, auth };
17 |
--------------------------------------------------------------------------------
/lib/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app';
2 | import 'firebase/auth'; // If you need it
3 | import 'firebase/firestore'; // If you need it
4 | // import 'firebase/storage' // If you need it
5 | // import 'firebase/analytics' // If you need it
6 | // import 'firebase/performance' // If you need it
7 |
8 | const clientCredentials = {
9 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
10 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
11 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
12 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
13 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
14 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
15 | };
16 |
17 | if (!firebase.apps.length) {
18 | firebase.initializeApp(clientCredentials);
19 | // Check that `window` is in scope for the analytics module!
20 | // if (typeof window !== 'undefined') {
21 | // // Enable analytics. https://firebase.google.com/docs/analytics/get-started
22 | // if ('measurementId' in clientCredentials) {
23 | // firebase.analytics()
24 | // firebase.performance()
25 | // }
26 | // }
27 | }
28 |
29 | export default firebase;
30 |
--------------------------------------------------------------------------------
/lib/middlewares.js:
--------------------------------------------------------------------------------
1 | import { auth } from '@/lib/firebase-admin';
2 |
3 | export function withAuth(handler) {
4 | return async (req, res) => {
5 | const authHeader = req.headers.authorization;
6 | if (!authHeader) {
7 | return res.status(401).end('Not authenticated. No Auth header');
8 | }
9 |
10 | const token = authHeader.split(' ')[1];
11 | let decodedToken;
12 | try {
13 | decodedToken = await auth.verifyIdToken(token);
14 | if (!decodedToken || !decodedToken.uid)
15 | return res.status(401).end('Not authenticated');
16 | req.uid = decodedToken.uid;
17 | } catch (error) {
18 | console.log(error.errorInfo);
19 | const errorCode = error.errorInfo.code;
20 | error.status = 401;
21 | if (errorCode === 'auth/internal-error') {
22 | error.status = 500;
23 | }
24 | //TODO handlle firebase admin errors in more detail
25 | return res.status(error.status).json({ error: errorCode });
26 | }
27 |
28 | return handler(req, res);
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | // next.config.js
2 | const withImages = require('next-images');
3 |
4 | const withMDX = require('@next/mdx')({
5 | extension: /\.mdx?$/,
6 | options: {
7 | remarkPlugins: [
8 | require('remark-slug'),
9 | [require('remark-autolink-headings'), { behavior: 'wrap' }],
10 | ],
11 | rehypePlugins: [],
12 | },
13 | });
14 |
15 | module.exports = withMDX(
16 | withImages({
17 | fileExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
18 | pageExtensions: ['js', 'jsx', 'mdx'],
19 | webpack(config, options) {
20 | return config;
21 | },
22 | async headers() {
23 | return [
24 | {
25 | // matching all API routes
26 | source: '/api/:path*',
27 | headers: [
28 | { key: 'Access-Control-Allow-Credentials', value: 'true' },
29 | { key: 'Access-Control-Allow-Origin', value: '*' },
30 | {
31 | key: 'Access-Control-Allow-Methods',
32 | value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT',
33 | },
34 | {
35 | key: 'Access-Control-Allow-Headers',
36 | value:
37 | 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version',
38 | },
39 | ],
40 | },
41 | ];
42 | },
43 | })
44 | );
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twittertool",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@chakra-ui/icons": "^1.0.5",
12 | "@chakra-ui/react": "^1.3.3",
13 | "@emotion/react": "^11.1.5",
14 | "@emotion/styled": "^11.1.5",
15 | "@mdx-js/loader": "^1.6.22",
16 | "@next/mdx": "^10.0.7",
17 | "firebase": "^8.2.9",
18 | "framer-motion": "^3.9.2",
19 | "next": "10.0.7",
20 | "next-images": "^1.7.0",
21 | "pino": "^6.11.1",
22 | "pino-logflare": "^0.3.7",
23 | "pino-pretty": "^4.6.0",
24 | "react": "17.0.1",
25 | "react-dom": "17.0.1",
26 | "react-icons": "^4.2.0",
27 | "remark-autolink-headings": "^6.0.1",
28 | "remark-slug": "^6.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { AuthProvider } from '@/lib/auth';
2 | import { ChakraProvider } from '@chakra-ui/react';
3 | import theme from '@/styles/theme';
4 | import { MDXProvider } from '@mdx-js/react';
5 | import MDXComponents from '@/components/MDXComponents';
6 | import React from 'react';
7 | // import { DefaultSeo } from 'next-seo';
8 |
9 | // const title =
10 | // 'Ghost Preview – Automatically add content preview for member-only posts on Ghost.';
11 | // const description =
12 | // 'Ghost Preview is one of the Ghost Utilities (Ghutils) that is built to make the blogging experience on Ghost platform better';
13 |
14 | // const SEO = {
15 | // title,
16 | // description,
17 | // canonical: 'https://ghutils.dingran.me',
18 | // openGraph: {
19 | // site_name: 'Ghost Preview',
20 | // type: 'website',
21 | // locale: 'en_US',
22 | // url: 'https://ghutils.dingran.me',
23 | // title,
24 | // description,
25 | // images: [
26 | // {
27 | // url: 'https://ghutils.dingran.me/og.jpg',
28 | // alt: title,
29 | // width: 523,
30 | // height: 274,
31 | // },
32 | // ],
33 | // },
34 | // twitter: {
35 | // handle: '@ding_ran',
36 | // site: '@ding_ran',
37 | // cardType: 'summary_large_image',
38 | // },
39 | // };
40 |
41 | const App = ({ Component, pageProps }) => {
42 | return (
43 |
44 |
45 |
46 | {/* */}
47 |
48 |
49 |
50 |
51 | );
52 | };
53 | export default App;
54 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document';
2 |
3 | class MyDocument extends Document {
4 | render() {
5 | return (
6 |
7 |
8 |
12 |
18 | {/* */}
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 | }
33 |
34 | export default MyDocument;
35 |
--------------------------------------------------------------------------------
/pages/account.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { pinoLogger } from '@/utils/logger';
3 | import {
4 | Avatar,
5 | Heading,
6 | Box,
7 | Button,
8 | Flex,
9 | Text,
10 | Badge,
11 | StatGroup,
12 | Stat,
13 | StatLabel,
14 | StatNumber,
15 | StatHelpText,
16 | BreadcrumbItem,
17 | Breadcrumb,
18 | BreadcrumbLink,
19 | } from '@chakra-ui/react';
20 |
21 | import { useAuth } from '@/lib/auth';
22 | // import { goToBillingPortal } from '@/lib/db';
23 | import PageShell from '@/components/PageShell';
24 | import NextBreadcrumb from '@/components/NextBreadcrumb';
25 | import Router from 'next/router';
26 |
27 | const SettingsTable = ({ stripeRole, children }) => (
28 |
34 |
43 |
44 |
51 | Settings
52 |
53 | {/*
54 | {stripeRole}
55 | */}
56 |
57 |
58 |
59 | {children}
60 |
61 |
62 | );
63 |
64 | const Account = () => {
65 | const { user, loading, signout } = useAuth();
66 | const [isBillingLoading, setBillingLoading] = useState(false);
67 | if (!loading && !user) {
68 | Router.push('/');
69 | }
70 |
71 | pinoLogger.info('pino logger from browser');
72 |
73 | return (
74 | <>
75 | {user ? (
76 |
77 |
81 |
87 |
88 |
94 | {user?.name}
95 | {user?.email}
96 |
97 |
98 | {/* */}
99 |
100 | This tool is free and currently doesn't have any
101 | settings/options yet. A logout button is the best we can offer
102 | 😀
103 |
104 |
105 |
108 | {/* */}
126 |
127 |
128 |
129 |
130 | ) : null}
131 | >
132 | );
133 | };
134 |
135 | const AccountPage = () => ;
136 |
137 | export default AccountPage;
138 |
--------------------------------------------------------------------------------
/pages/dashboard.js:
--------------------------------------------------------------------------------
1 | import PageShell from '@/components/PageShell';
2 |
3 | export default function Dashboard() {
4 | return (
5 | <>
6 | Dashbaord Page
7 | >
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/pages/faq.mdx:
--------------------------------------------------------------------------------
1 | import PageShell from '@/components/PageShell';
2 | import HiddenEmail from '@/components/HiddenEmail';
3 | import LoginButtons from '@/components/LoginButtons';
4 | import NextBreadcrumb from '@/components/NextBreadcrumb';
5 | import { HStack } from '@chakra-ui/react';
6 | import BMAC from '@/components/BMAC';
7 |
8 | export default (props) => ;
9 |
10 |
11 |
12 | # FAQ
13 |
14 | ## Why?
15 |
16 | ## How to set it up?
17 |
18 | Blah blah
19 |
20 | ## Why is it free?
21 |
22 | It was simple to build and free to host, so it's free for fair use.
23 |
24 |
25 | If you insist 🤩, feel free to buy me a coffee here
26 |
27 |
28 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Hero from '@/components/Hero';
2 | import PageShell from '@/components/PageShell';
3 |
4 | export default function Home() {
5 | return (
6 | <>
7 |
8 |
9 |
10 | >
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/pages/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAuth } from '@/lib/auth';
3 | import { Box } from '@chakra-ui/react';
4 | import LoginButtons from '@/components/LoginButtons';
5 |
6 | export default function Home() {
7 | const auth = useAuth();
8 | return (
9 | <>
10 |
11 |
12 | {auth.user ? (
13 |
14 |
Email: {auth.user.email}
15 |
16 |
17 | ) : (
18 |
19 | )}
20 |
21 | >
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dingran/nextjs-starter-template/34760d77671d21a6e514c994bf223cbca96b9760/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles/theme.js:
--------------------------------------------------------------------------------
1 | import { extendTheme } from '@chakra-ui/react';
2 |
3 | const theme = extendTheme({
4 | styles: {
5 | global: {
6 | '.cryptedmail:after': {
7 | content: `attr(data-name) "@" attr(data-domain) "." attr(data-tld);`,
8 | },
9 | a: {
10 | textDecor: 'underline',
11 | },
12 | 'h1 > a, h2 > a, h3 > a, h4 > a': {
13 | textDecor: 'none',
14 | },
15 | 'a:hover': {
16 | color: 'blue.500',
17 | },
18 | fonts: {
19 | heading: 'Georgia, serif',
20 | mono: 'Menlo, monospace',
21 | },
22 | 'h1, h2, h3, h4, h5, h6': {
23 | mt: '1.5em',
24 | mb: '0.8em',
25 | },
26 | 'ol, ul': {
27 | pl: '1.0em',
28 | pr: '1.0em',
29 | listStylePosition: 'inside',
30 | },
31 | 'p, ul, ol, dl, blockquote': {
32 | mt: '1.0em',
33 | mb: '1.0em',
34 | },
35 | blockquote: {
36 | px: '1.0em',
37 | borderLeftColor: 'blue.200',
38 | borderLeftWidth: '3px',
39 | textColor: 'gray.400',
40 | },
41 | svg: {
42 | display: 'inline',
43 | },
44 | },
45 | },
46 | fonts: {
47 | body: `Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"`,
48 | },
49 | colors: {
50 | brand: {
51 | 50: '#F7FAFC',
52 | 100: '#EDF2F7',
53 | 200: '#E2E8F0',
54 | 300: '#CBD5E0',
55 | 400: '#A0AEC0',
56 | 500: '#718096',
57 | 600: '#4A5568',
58 | 700: '#2D3748',
59 | 800: '#1A202C',
60 | 900: '#171923',
61 | },
62 | },
63 | fontWeights: {
64 | normal: 400,
65 | medium: 600,
66 | bold: 800,
67 | },
68 | });
69 | export default theme;
70 |
--------------------------------------------------------------------------------
/utils/fetcher.js:
--------------------------------------------------------------------------------
1 | const fetcher = async (url, token) => {
2 | const res = await fetch(url, {
3 | method: 'GET',
4 | headers: new Headers({
5 | 'Content-Type': 'application/json',
6 | Authorization: `Bearer ${token}`, //this is proper but harder to parse
7 | token,
8 | }),
9 | credentials: 'same-origin',
10 | });
11 |
12 | if (!res.ok) {
13 | const error = new Error('An error occurred while fetching the data.');
14 | // Attach extra info to the error object.
15 | error.info = await res.json();
16 | error.status = res.status;
17 | throw error;
18 | // throw error would cause useSWR to retry, and it seems to fix the issue of firebase expired token
19 | }
20 |
21 | return res.json();
22 | };
23 |
24 | export default fetcher;
25 |
--------------------------------------------------------------------------------
/utils/logger.js:
--------------------------------------------------------------------------------
1 | import pino from 'pino';
2 | import { logflarePinoVercel } from 'pino-logflare';
3 |
4 | const { stream, send } = logflarePinoVercel({
5 | apiKey: process.env.NEXT_PUBLIC_LOGFLARE_KEY,
6 | sourceToken: process.env.NEXT_PUBLIC_LOGFLARE_STREAM,
7 | });
8 |
9 | const config = {
10 | browser: {
11 | transmit: {
12 | level: 'info',
13 | send: send,
14 | },
15 | },
16 | level: 'debug',
17 | base: {
18 | env: process.env.NODE_ENV || 'ENV not set',
19 | revision: process.env.VERCEL_GITHUB_COMMIT_SHA,
20 | },
21 | };
22 |
23 | let pinoLogger;
24 | if (process.env.NODE_ENV === 'production') {
25 | pinoLogger = pino(config, stream);
26 | } else {
27 | config.prettyPrint = true;
28 | pinoLogger = pino(config);
29 | }
30 |
31 | const formatObjectKeys = (headers) => {
32 | const keyValues = {};
33 |
34 | Object.keys(headers).map((key) => {
35 | const newKey = key.replace(/-/g, '_');
36 | keyValues[newKey] = headers[key];
37 | });
38 |
39 | return keyValues;
40 | };
41 |
42 | function logError(req, res, errorObj, msg) {
43 | pinoLogger.error(
44 | {
45 | errorObj,
46 | request: {
47 | url: req.url,
48 | method: req.method,
49 | headers: formatObjectKeys(req.headers),
50 | },
51 | response: {
52 | statusCode: res.statusCode,
53 | },
54 | },
55 | msg
56 | );
57 | }
58 |
59 | export { pinoLogger, logError, formatObjectKeys };
60 |
--------------------------------------------------------------------------------