├── .env.example
├── .gitignore
├── README.md
├── components
├── Table
│ ├── BasicTable.js
│ └── Table.js
└── layout
│ ├── ActiveLink.js
│ ├── Layout.js
│ └── Navbar.js
├── context
├── Actions.js
├── GlobalState.js
└── Reducers.js
├── jsconfig.json
├── lib
└── gtag.js
├── models
├── School.js
└── User.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── _document.js
├── api
│ ├── auth
│ │ └── [...nextauth].js
│ ├── hello.js
│ └── schools
│ │ └── index.js
├── dashboard.js
├── index.js
└── profile.js
├── postcss.config.js
├── public
├── favicon.ico
└── vercel.svg
├── styles
└── globals.css
├── tailwind.config.js
└── utils
├── dbConnect.js
├── scrollRestoration.js
├── usePaginatedSchools.js
└── useSchools.js
/.env.example:
--------------------------------------------------------------------------------
1 | MONGODB_URI=
2 |
--------------------------------------------------------------------------------
/.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 | # Next.js Starter | MongoDB | Tailwind | NextAuth
2 |
3 | This is a custom NextJS Starter with these technologies set up:
4 |
5 | - MongoDB
6 | - Mongoose
7 | - TailwindCSS
8 | - NextAuth with Google Sign In
9 |
10 | ## How to use
11 |
12 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
13 |
14 | ```bash
15 | npx create-next-app myapp -e https://github.com/uguremirmustafa/with-tailwind-mongo-next-auth
16 | # or
17 | yarn create next-app myapp -e https://github.com/uguremirmustafa/with-tailwind-mongo-next-auth
18 | ```
19 |
20 | ## Small Details
21 |
22 | - This project includes small script to restore scroll position on route change for nextjs.
23 | - Includes a modular navbar with indicator of the active page.
24 | - Example `env` file.
25 | - Smart importing with `jsconfig.json` file.
26 |
27 | ## Todo
28 |
29 | - Write down small docs to starter
30 | - Add a loading spinner.
31 | - Create data fetching functions
32 | - Add api routes
33 | - Add a header component to show title of the page
34 | - Add favicon
35 | - Remove console.logs from custom scrolling function
36 |
--------------------------------------------------------------------------------
/components/Table/BasicTable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTable } from 'react-table';
3 |
4 | export default function BasicTable({ data, columns }) {
5 | const { getTableProps, getTableBodyProps, headerGroups, prepareRow, rows } = useTable({
6 | columns,
7 | data,
8 | });
9 | return (
10 | <>
11 |
12 |
13 | {headerGroups.map((headerGroup) => (
14 |
15 | {headerGroup.headers.map((column) => (
16 |
17 | {column.render('Header')}
18 | {/* {column.isSorted ? (column.isSortedDesc ? 'as 🔽' : ' 🔼') : ''} */}
19 | |
20 | ))}
21 |
22 | ))}
23 |
24 |
25 | {rows.map((row, i) => {
26 | prepareRow(row);
27 | return (
28 |
29 | {row.cells.map((cell) => {
30 | return {cell.render('Cell')} | ;
31 | })}
32 |
33 | );
34 | })}
35 |
36 |
37 | >
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/components/Table/Table.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTable, usePagination } from 'react-table';
3 |
4 | function Table({ setPerPage, setPage, columns, data, currentpage, perPage, totalPage }) {
5 | const {
6 | getTableProps,
7 | getTableBodyProps,
8 | headerGroups,
9 | prepareRow,
10 | page,
11 | // canPreviousPage,
12 | // canNextPage,
13 | pageOptions,
14 | // pageCount,
15 | // gotoPage,
16 | // nextPage,
17 | // previousPage,
18 | // setPageSize,
19 | // Get the state from the instance
20 | state: { pageIndex, pageSize },
21 | } = useTable(
22 | {
23 | columns,
24 | data,
25 | useControlledState: (state) => {
26 | return React.useMemo(
27 | () => ({
28 | ...state,
29 | pageIndex: currentpage,
30 | }),
31 | [state, currentpage]
32 | );
33 | },
34 | initialState: { pageIndex: currentpage }, // Pass our hoisted table state
35 | manualPagination: true, // Tell the usePagination
36 | // hook that we'll handle our own data fetching
37 | // This means we'll also have to provide our own
38 | // pageCount.
39 | pageCount: totalPage,
40 | },
41 | usePagination
42 | );
43 |
44 | return (
45 | <>
46 |
47 |
48 | {headerGroups.map((headerGroup) => (
49 |
50 | {headerGroup.headers.slice(0, 1).map((column) => (
51 |
55 | {column.render('Header')}
56 | |
57 | ))}
58 | {headerGroup.headers.slice(1).map((column) => (
59 |
63 | {column.render('Header')}
64 | |
65 | ))}
66 |
67 | ))}
68 |
69 |
70 | {page.map((row, i) => {
71 | prepareRow(row);
72 | return (
73 |
74 | {row.cells.map((cell) => {
75 | return (
76 |
77 | {cell.render('Cell')}
78 | |
79 | );
80 | })}
81 |
82 | );
83 | })}
84 |
85 |
86 |
87 |
88 | {' '}
96 | {' '}
104 | {' '}
112 | {' '}
120 |
121 | Page{' '}
122 |
123 | {pageIndex} of {pageOptions.length}
124 | {' '}
125 |
126 |
127 | | Go to page:{' '}
128 | {
134 | const page = e.target.value ? Number(e.target.value) : 1;
135 | setPage(page);
136 | }}
137 | className="w-20 border-2 rounded px-2"
138 | />
139 | {' '}
140 |
153 |
154 | >
155 | );
156 | }
157 |
158 | export default Table;
159 |
--------------------------------------------------------------------------------
/components/layout/ActiveLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 |
5 | export const ActiveLink = ({ href, children }) => {
6 | const router = useRouter();
7 |
8 | let className = children.props.className || '';
9 | if (router.pathname === href) {
10 | className = `${className} selected`;
11 | }
12 |
13 | return (
14 |
15 | {React.cloneElement(children, { className })}
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/components/layout/Layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Navbar from './Navbar';
3 |
4 | function Layout({ children }) {
5 | return (
6 |
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
13 | export default Layout;
14 |
--------------------------------------------------------------------------------
/components/layout/Navbar.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import React from 'react';
3 | import { signIn, signOut, useSession } from 'next-auth/client';
4 | import { ActiveLink } from './ActiveLink';
5 |
6 | function Navbar() {
7 | const [session, loading] = useSession();
8 |
9 | const protectedRoutes = [
10 | { route: '/profile', label: 'Profil' },
11 | { route: '/dashboard', label: 'dashboard' },
12 | ];
13 | const normalRoutes = [
14 | { route: '/', label: 'Home' },
15 | { route: '/dashboard', label: 'Dashboard' },
16 | ];
17 |
18 | const protectedLinks = protectedRoutes.map((i) => (
19 |
20 | {i.label}
21 |
22 | ));
23 |
24 | const normalLinks = normalRoutes.map((i) => (
25 |
26 | {i.label}
27 |
28 | ));
29 | return (
30 |
57 | );
58 | }
59 |
60 | export default Navbar;
61 |
--------------------------------------------------------------------------------
/context/Actions.js:
--------------------------------------------------------------------------------
1 | const ACTIONS = {
2 | TOGGLE_MODAL: 'TOGGLE_MODAL',
3 | };
4 | export default ACTIONS;
5 |
--------------------------------------------------------------------------------
/context/GlobalState.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useReducer } from 'react';
2 | import reducers from './Reducers';
3 | export const AppContext = createContext();
4 |
5 | export const DataProvider = ({ children }) => {
6 | const initialState = {
7 | modalOpen: false,
8 | };
9 | const [state, dispatch] = useReducer(reducers, initialState);
10 |
11 | return {children};
12 | };
13 |
--------------------------------------------------------------------------------
/context/Reducers.js:
--------------------------------------------------------------------------------
1 | import ACTIONS from './Actions';
2 |
3 | const reducers = (state, action) => {
4 | switch (action.type) {
5 | case ACTIONS.OPEN_MODAL:
6 | return {
7 | ...state,
8 | modalOpen: action.payload,
9 | };
10 |
11 | default:
12 | break;
13 | }
14 | };
15 | export default reducers;
16 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@components/*": ["components/*"],
6 | "@styles/*": ["styles/*"],
7 | "@utils/*": ["utils/*"],
8 | "@models/*": ["models/*"],
9 | "@context/*": ["context/*"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/gtag.js:
--------------------------------------------------------------------------------
1 | export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GOOGLE_TRACKER_ID;
2 |
3 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages
4 | export const pageview = (url) => {
5 | window.gtag('config', GA_TRACKING_ID, {
6 | page_path: url,
7 | });
8 | };
9 |
10 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events
11 | export const event = ({ action, category, label, value }) => {
12 | window.gtag('event', action, {
13 | event_category: category,
14 | event_label: label,
15 | value: value,
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/models/School.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import mongoosePaginate from 'mongoose-paginate-v2';
3 | const SchoolSchema = new mongoose.Schema(
4 | {
5 | name: {
6 | type: 'String',
7 | lowercase: true,
8 | },
9 | il: 'String',
10 | ilce: 'String',
11 | kont: 'Number',
12 | tercihEdenler: [
13 | {
14 | type: mongoose.Types.ObjectId,
15 | ref: 'user',
16 | },
17 | ],
18 | yorumlar: [
19 | {
20 | kullanici: 'String',
21 | yorum: 'String',
22 | },
23 | { timestamps: true },
24 | ],
25 | },
26 | {
27 | timestamps: true,
28 | }
29 | );
30 | SchoolSchema.plugin(mongoosePaginate);
31 | export default mongoose.models.School || mongoose.model('School', SchoolSchema);
32 |
--------------------------------------------------------------------------------
/models/User.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const userSchema = new mongoose.Schema(
4 | {
5 | name: String,
6 | email: String,
7 | image: String,
8 | },
9 | {
10 | timestamps: true,
11 | }
12 | );
13 |
14 | export default mongoose.models.user || mongoose.model('user', userSchema);
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-tailwindcss",
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 | "autoprefixer": "^10.0.4",
12 | "axios": "^0.21.1",
13 | "mongodb": "^3.6.4",
14 | "mongoose": "^5.11.18",
15 | "mongoose-paginate-v2": "^1.3.16",
16 | "next": "latest",
17 | "next-auth": "^3.6.0",
18 | "postcss": "^8.1.10",
19 | "react": "^17.0.1",
20 | "react-dom": "^17.0.1",
21 | "react-query": "^3.12.0",
22 | "react-table": "^7.6.3",
23 | "tailwindcss": "^2.0.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { Provider } from 'next-auth/client';
2 | import '../styles/globals.css';
3 | import { DataProvider } from '@context/GlobalState';
4 | import Layout from '@components/layout/Layout';
5 | import { initRouterListeners } from '@utils/scrollRestoration';
6 | import { QueryClient, QueryClientProvider } from 'react-query';
7 |
8 | initRouterListeners();
9 |
10 | function MyApp({ Component, pageProps }) {
11 | const queryClient = new QueryClient();
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default MyApp;
27 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import Document, { Html, Head, Main, NextScript } from 'next/document';
4 |
5 | import { GA_TRACKING_ID } from '../lib/gtag';
6 |
7 | class MyDocument extends Document {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | export default MyDocument;
36 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].js:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 | import Providers from 'next-auth/providers';
3 |
4 | export default NextAuth({
5 | providers: [
6 | Providers.Google({
7 | clientId: process.env.GOOGLE_ID,
8 | clientSecret: process.env.GOOGLE_SECRET,
9 | }),
10 | ],
11 | database: process.env.MONGODB_URI,
12 | pages: {
13 | // signIn: '/giris',
14 | // newUser: '/profile',
15 | },
16 | debug: process.env.NODE_ENV === 'development',
17 | // secret: process.env.AUTH_SECRET,
18 | // jwt: {
19 | // secret: process.env.JWT_SECRET,
20 | // },
21 | });
22 |
--------------------------------------------------------------------------------
/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | export default function helloAPI(req, res) {
4 | res.status(200).json({ name: 'John Doe' })
5 | }
6 |
--------------------------------------------------------------------------------
/pages/api/schools/index.js:
--------------------------------------------------------------------------------
1 | import School from '@models/School';
2 | import dbConnect from '@utils/dbConnect';
3 |
4 | dbConnect();
5 |
6 | export default async function (req, res) {
7 | switch (req.method) {
8 | case 'GET':
9 | await getSchools(req, res);
10 | break;
11 |
12 | default:
13 | res.status(400).json({ success: false });
14 | break;
15 | }
16 | }
17 | const getSchools = async (req, res) => {
18 | try {
19 | let { page, perPage } = req.query;
20 | console.log(page, perPage);
21 | const options = {
22 | page: parseInt(page),
23 | limit: parseInt(perPage),
24 | };
25 | const schools = await School.paginate({}, options);
26 | res.status(200).json({
27 | success: true,
28 | data: schools,
29 | });
30 | } catch (error) {
31 | res.status(400).json({ success: false });
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/pages/dashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function dashboard() {
4 | return dashboard
;
5 | }
6 |
7 | export default dashboard;
8 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Table from '@components/Table/Table';
3 | import useSchools from '@utils/useSchools';
4 |
5 | export default function Home() {
6 | const [page, setPage] = useState(1);
7 | const [perPage, setPerPage] = useState(10);
8 | const { data: schools, isLoading } = useSchools(page, perPage);
9 | const list = schools?.data.docs.map((i) => {
10 | return {
11 | col1: i.name,
12 | col2: i.il,
13 | col3: i.ilce,
14 | col4: i.kont,
15 | };
16 | });
17 | const data = React.useMemo(() => list, [schools]);
18 | const columns = React.useMemo(
19 | () => [
20 | {
21 | Header: 'okul adi',
22 | accessor: 'col1', // accessor is the "key" in the data
23 | },
24 | {
25 | Header: 'il',
26 | accessor: 'col2',
27 | },
28 | {
29 | Header: 'ilce',
30 | accessor: 'col3',
31 | },
32 | {
33 | Header: 'kontenjan',
34 | accessor: 'col4',
35 | },
36 | ],
37 | []
38 | );
39 | if (isLoading) return loading...
;
40 | return (
41 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/pages/profile.js:
--------------------------------------------------------------------------------
1 | import Navbar from '@components/layout/Navbar';
2 | import Link from 'next/link';
3 | import React from 'react';
4 |
5 | function profile() {
6 | return (
7 |
8 |
hello from profile
9 |
10 | );
11 | }
12 |
13 | export default profile;
14 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | // If you want to use other PostCSS plugins, see the following:
2 | // https://tailwindcss.com/docs/using-with-preprocessors
3 | module.exports = {
4 | plugins: {
5 | tailwindcss: {},
6 | autoprefixer: {},
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uguremirmustafa/server-side-paginated-table-with-react-table/c863aa15fe94696ee36c204204b673d60f40767c/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 |
4 | @layer components {
5 | .btn-sm {
6 | @apply bg-blue-500 font-bold text-white rounded-md py-1 px-3 text-center hover:bg-blue-400 cursor-pointer transition duration-300 text-sm shadow-md hover:shadow-lg;
7 | }
8 | .btn-md {
9 | @apply bg-blue-500 font-bold text-white rounded-md py-2 px-4 text-center hover:bg-blue-400 cursor-pointer transition duration-300 text-base shadow-md hover:shadow-lg;
10 | }
11 | .btn-lg {
12 | @apply bg-blue-500 font-bold text-white rounded-md py-3 px-8 text-center hover:bg-blue-400 cursor-pointer transition duration-300 text-lg shadow-md hover:shadow-lg;
13 | }
14 | .nav-btn {
15 | @apply bg-white font-bold text-gray-900 rounded py-1 px-3 text-center hover:bg-gray-900 hover:text-white cursor-pointer transition duration-300 ml-1 capitalize;
16 | }
17 | .selected {
18 | @apply underline;
19 | }
20 | }
21 |
22 | @tailwind utilities;
23 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {},
6 | },
7 | variants: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
12 |
--------------------------------------------------------------------------------
/utils/dbConnect.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | async function dbConnect() {
4 | // check if we have a connection to the database or if it's currently
5 | // connecting or disconnecting (readyState 1, 2 and 3)
6 | if (mongoose.connection.readyState >= 1) {
7 | return;
8 | }
9 |
10 | return mongoose.connect(process.env.MONGODB_URI, {
11 | useNewUrlParser: true,
12 | useUnifiedTopology: true,
13 | useFindAndModify: false,
14 | useCreateIndex: true,
15 | });
16 | }
17 |
18 | export default dbConnect;
19 |
--------------------------------------------------------------------------------
/utils/scrollRestoration.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('next/router');
2 |
3 | // initRouterListeners();
4 |
5 | export function initRouterListeners() {
6 | console.log('Init router listeners');
7 |
8 | const routes = [];
9 |
10 | Router.events.on('routeChangeStart', (url) => {
11 | pushCurrentRouteInfo();
12 | });
13 |
14 | Router.events.on('routeChangeComplete', (url) => {
15 | fixScrollPosition();
16 | });
17 |
18 | // Hack to set scrollTop because of this issue:
19 | // - https://github.com/zeit/next.js/issues/1309
20 | // - https://github.com/zeit/next.js/issues/3303
21 |
22 | function pushCurrentRouteInfo() {
23 | routes.push({ pathname: Router.pathname, scrollY: window.scrollY });
24 | }
25 |
26 | // TODO: We guess we're going back, but there must be a better way
27 | // https://github.com/zeit/next.js/issues/1309#issuecomment-435057091
28 | function isBack() {
29 | return routes.length >= 2 && Router.pathname === routes[routes.length - 2].pathname;
30 | }
31 |
32 | function fixScrollPosition() {
33 | let scrollY = 0;
34 |
35 | if (isBack()) {
36 | routes.pop(); // route where we come from
37 | const targetRoute = routes.pop(); // route where we return
38 | scrollY = targetRoute.scrollY; // scrollY we had before
39 | }
40 |
41 | console.log('Scrolling to', scrollY);
42 | window.requestAnimationFrame(() => window.scrollTo(0, scrollY));
43 | console.log('routes now:', routes);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/utils/usePaginatedSchools.js:
--------------------------------------------------------------------------------
1 | const { useQuery } = require('react-query');
2 | import axios from 'axios';
3 |
4 | export default function useSchools(page, perPage) {
5 | return useQuery(['schools', page, perPage], async () => {
6 | const res = await axios.get(`/api/schools?perPage=${perPage}&page=${page}`);
7 | return res.data;
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/utils/useSchools.js:
--------------------------------------------------------------------------------
1 | const { useQuery } = require('react-query');
2 | const axios = require('axios');
3 |
4 | export default function useSchools(page, perPage) {
5 | return useQuery(
6 | ['schools', page, perPage],
7 | async () => {
8 | const res = await axios.get(`/api/schools?perPage=${perPage}&page=${page}`);
9 | return res.data;
10 | },
11 | { keepPreviousData: true }
12 | );
13 | }
14 |
--------------------------------------------------------------------------------