├── data-layer ├── strapi │ ├── company.ts │ └── contacts.ts ├── apiClient.ts ├── apiClient_later.ts ├── index.ts.old.ts └── index.ts ├── public ├── favicon.ico ├── vercel.svg └── next.svg ├── postcss.config.js ├── pages ├── demo │ ├── demo.module.scss │ └── index.tsx ├── _document.tsx ├── api │ ├── test-strapi.ts │ └── hello.ts ├── _app.tsx ├── template │ └── index.tsx ├── blog │ ├── index.tsx │ └── testPage.tsx ├── products │ └── index.tsx ├── users │ ├── index.tsx │ └── [id].tsx ├── contacts │ └── index.tsx ├── daisyui │ └── index.tsx └── index.tsx ├── next.config.js ├── contexts └── ThemeContext.tsx ├── components ├── layouts │ ├── Footerbar.tsx │ ├── Navbar.module.scss │ ├── Main.tsx │ ├── Box.tsx │ ├── index.ts │ ├── Row.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── Container.tsx │ └── Navbar.tsx ├── ui-ux │ ├── ListGroup.scss │ ├── ThemeSwitch.tsx │ ├── Alert.tsx │ ├── Button.tsx │ ├── ProductSearchBar.tsx │ ├── UserSearchBar.tsx │ ├── Contact.scss │ ├── RadioButtonCategoryGroup.tsx │ ├── RadioGroupGender.tsx │ ├── ListGroup.tsx │ ├── Contact.tsx │ ├── RadioGroupCreditCard.tsx │ ├── RadioGroupCurrency.tsx │ └── Spinner.tsx └── Layout.tsx ├── .env.local.example ├── features ├── posts │ ├── apiPosts.ts │ └── Posts.tsx ├── products │ ├── productSlice.ts │ ├── apiProducts.ts │ └── Products.tsx └── users │ ├── userSlice.tsx │ ├── apiUsers.ts │ └── Users.tsx ├── theme └── ThemeProvider.tsx ├── styles └── globals.scss ├── .gitignore ├── .env ├── archive ├── apiProducts-1-keep.ts ├── userSlice-1-keep.tsx └── apiUsers-1-keep.ts ├── tsconfig.json ├── tailwind.config.js ├── global-interfaces.ts ├── store └── store.ts ├── package.json └── README.md /data-layer/strapi/company.ts: -------------------------------------------------------------------------------- 1 | export const nothing = ""; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedmusawir/starter-next13-pages-ts-v1/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /pages/demo/demo.module.scss: -------------------------------------------------------------------------------- 1 | .moose-flex { 2 | // border: 1rem dotted red; 3 | display: flex; 4 | flex-wrap: wrap; 5 | } 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /data-layer/apiClient.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const strapiAPIKey = process.env.STRAPI_API_KEY; 3 | axios.defaults.headers.common["Authorization"] = `Bearer ${strapiAPIKey}`; 4 | 5 | export default axios; 6 | -------------------------------------------------------------------------------- /contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ThemeContext = React.createContext<{ 4 | theme: string; 5 | setTheme: (theme: string) => void; 6 | }>({ 7 | theme: "light", 8 | setTheme: (theme) => console.warn("no theme provider"), 9 | }); 10 | -------------------------------------------------------------------------------- /components/layouts/Footerbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footerbar = () => { 4 | const today = new Date(); 5 | const currentYear = today.getFullYear(); 6 | return
© Copyright {currentYear}
; 7 | }; 8 | 9 | export default Footerbar; 10 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components/layouts/Navbar.module.scss: -------------------------------------------------------------------------------- 1 | .menu li:active a { 2 | @apply bg-indigo-200; 3 | } 4 | .menu li:active { 5 | @apply rounded-none; 6 | @apply bg-indigo-200; 7 | } 8 | 9 | .menu li a:hover { 10 | border-radius: 2rem !important; 11 | } 12 | .menu li a:focus { 13 | border-radius: 0.5rem !important; 14 | } 15 | -------------------------------------------------------------------------------- /components/ui-ux/ListGroup.scss: -------------------------------------------------------------------------------- 1 | .list-group { 2 | border: 1px solid #e3e3e3; 3 | padding: 2rem; 4 | 5 | .list-group-item { 6 | background-color: #e3e3e3; 7 | padding: 1rem; 8 | list-style: none; 9 | } 10 | 11 | .active { 12 | background-color: dodgerblue; 13 | color: #e3e3e3; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/api/test-strapi.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import datasource from "../../data-layer"; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | const data = await datasource.getContacts(); 9 | res.status(200).json({ data }); 10 | } 11 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | DATALAYER_ENGINE=strapi|contentful|datocms|sanity|prisma|faunadb|etc... 2 | 3 | #### STRAPI CMS ENV VARS ### 4 | STRAPI_API_KEY=[CHANGE] 5 | 6 | # LOCALHOST ONLY VALUES # 7 | STRAPI_ASSETS_BASE_URL=http://127.0.0.1:1337 8 | STRAPI_API_BASE_URL=http://127.0.0.1:1337/api 9 | STRAPI_IMAGES_DOMAIN=127.0.0.1 10 | 11 | 12 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /components/layouts/Main.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | className: string; 6 | } 7 | 8 | function Main({ children, className }: Props) { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | } 15 | 16 | export default Main; 17 | -------------------------------------------------------------------------------- /data-layer/apiClient_later.ts: -------------------------------------------------------------------------------- 1 | import axios, { CanceledError } from "axios"; 2 | 3 | // const apiKey = process.env.REACT_APP_GHL_API_KEY; 4 | 5 | const apiBaseURL = "http://localhost:1337"; 6 | 7 | const apiClient = axios.create({ 8 | baseURL: apiBaseURL, 9 | // headers: { Authorization: `Bearer ${apiKey}` }, 10 | }); 11 | 12 | export default apiClient; 13 | export { CanceledError }; 14 | -------------------------------------------------------------------------------- /components/layouts/Box.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | className?: string; 6 | } 7 | 8 | function Box({ children, className }: Props) { 9 | return ( 10 |
11 | {children ? children : "This is a Box container. Must have children"} 12 |
13 | ); 14 | } 15 | 16 | export default Box; 17 | -------------------------------------------------------------------------------- /components/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from "./Header"; 2 | export { default as Main } from "./Main"; 3 | export { default as Footer } from "./Footer"; 4 | export { default as Footerbar } from "./Footerbar"; 5 | export { default as Container } from "./Container"; 6 | export { default as Row } from "./Row"; 7 | export { default as Box } from "./Box"; 8 | export { default as Navbar } from "./Navbar"; 9 | -------------------------------------------------------------------------------- /components/layouts/Row.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | className?: string; 6 | } 7 | 8 | function Row({ children, className }: Props) { 9 | return ( 10 |
11 | {children ? children : "This is a Row container. Must have children"} 12 |
13 | ); 14 | } 15 | 16 | export default Row; 17 | -------------------------------------------------------------------------------- /components/layouts/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | className: string; 6 | } 7 | 8 | function Footer({ children, className }: Props) { 9 | return ( 10 |
11 | {children ? children : "This is a Footer container. Must have children"} 12 |
13 | ); 14 | } 15 | 16 | export default Footer; 17 | -------------------------------------------------------------------------------- /components/layouts/Header.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | className: string; 6 | } 7 | 8 | function Header({ children, className }: Props) { 9 | return ( 10 |
11 | {children ? children : "This is a Header container. Must have children"} 12 |
13 | ); 14 | } 15 | 16 | export default Header; 17 | -------------------------------------------------------------------------------- /features/posts/apiPosts.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export const apiPost = createApi({ 4 | reducerPath: "apiPost", 5 | baseQuery: fetchBaseQuery({ 6 | baseUrl: "https://dummyjson.com", 7 | }), 8 | endpoints: (builder) => ({ 9 | getPosts: builder.query({ 10 | query: () => `posts?limit=10`, 11 | }), 12 | }), 13 | }); 14 | 15 | export const { useGetPostsQuery } = apiPost; 16 | -------------------------------------------------------------------------------- /theme/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useState } from "react"; 2 | import { ThemeContext } from "@/contexts/ThemeContext"; 3 | 4 | interface Props { 5 | children: ReactNode; 6 | } 7 | 8 | export const ThemeProvider = ({ children }: Props) => { 9 | const [theme, setTheme] = useState("light"); 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { store } from "@/store/store"; 2 | import "@/styles/globals.scss"; 3 | import { ThemeProvider } from "@/theme/ThemeProvider"; 4 | import type { AppProps } from "next/app"; 5 | import { Provider } from "react-redux"; 6 | 7 | export default function App({ Component, pageProps }: AppProps) { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /data-layer/index.ts.old.ts: -------------------------------------------------------------------------------- 1 | import * as strapiContactAPI from "./strapi/contacts"; 2 | import * as strapiCompanyAPI from "./strapi/company"; 3 | 4 | interface DataSource { 5 | getContacts?: () => Promise; 6 | // define any other methods that your datasource may have 7 | } 8 | 9 | let datasource: DataSource = {}; 10 | 11 | if (process.env.DATALAYER_ENGINE === "strapi") 12 | datasource = { ...strapiCompanyAPI, ...strapiContactAPI }; 13 | 14 | export default datasource; 15 | -------------------------------------------------------------------------------- /components/ui-ux/ThemeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeContext } from "@/contexts/ThemeContext"; 2 | import { useContext } from "react"; 3 | 4 | function ThemeSwitch() { 5 | const { theme, setTheme } = useContext(ThemeContext); 6 | 7 | const toggleTheme = () => { 8 | setTheme(theme === "light" ? "dark" : "light"); 9 | }; 10 | 11 | return ( 12 | 15 | ); 16 | } 17 | 18 | export default ThemeSwitch; 19 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | // :root { 6 | // --foreground-rgb: 0, 0, 0; 7 | // --background-start-rgb: 214, 219, 220; 8 | // --background-end-rgb: 255, 255, 255; 9 | // } 10 | 11 | // body { 12 | // color: rgb(var(--foreground-rgb)); 13 | // background: linear-gradient( 14 | // to bottom, 15 | // transparent, 16 | // rgb(var(--background-end-rgb)) 17 | // ) 18 | // rgb(var(--background-start-rgb)); 19 | // } 20 | -------------------------------------------------------------------------------- /.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 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATALAYER_ENGINE=strapi|contentful|datocms|sanity|prisma|faunadb|etc... 2 | 3 | #### STRAPI CMS ENV VARS ### 4 | STRAPI_API_KEY=3515518bd1b2584de87a9c742b31b399f80dbb968e6479906d09a3efbf12bfc7fca5b0516c972f9a2ad46ec46ed2cbaf9dc22fedf6989e2c86502a2298effd1e2654d38c8a90e3e8f3b1ccc1acd55761c42e935705adbfbe8dc98abbdcc4c47347ba307aaed1a9d61280ac6e321447e0302fa027c1d24a6c80ac5c2e008a3b08 5 | 6 | # LOCALHOST ONLY VALUES # 7 | STRAPI_ASSETS_BASE_URL=http://127.0.0.1:1337 8 | STRAPI_API_BASE_URL=http://127.0.0.1:1337/api 9 | STRAPI_IMAGES_DOMAIN=127.0.0.1 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /archive/apiProducts-1-keep.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export const apiProducts = createApi({ 4 | reducerPath: "apiProducts", 5 | baseQuery: fetchBaseQuery({ 6 | baseUrl: "https://dummyjson.com", 7 | }), 8 | endpoints: (builder) => ({ 9 | getProducts: builder.query({ 10 | query: (searchTerm) => { 11 | if (searchTerm) { 12 | return `products/search?q=${searchTerm}`; 13 | } 14 | 15 | return `products?limit=10`; 16 | }, 17 | }), 18 | }), 19 | }); 20 | 21 | export const { useGetProductsQuery } = apiProducts; 22 | -------------------------------------------------------------------------------- /data-layer/index.ts: -------------------------------------------------------------------------------- 1 | // Import your APIs directly 2 | import { 3 | getContacts, 4 | ApiResponse as ContactApiResponse, 5 | } from "./strapi/contacts"; 6 | // import { getCompanies, ApiResponse as CompanyApiResponse } from './strapi/company'; 7 | 8 | // Define the DataSource interface 9 | export interface DataSource { 10 | getContacts: () => Promise; 11 | // getCompanies: () => Promise; 12 | } 13 | 14 | // Directly create a datasource object with your APIs 15 | const datasource: DataSource = { 16 | getContacts, 17 | // getCompanies, 18 | }; 19 | 20 | export default datasource; 21 | -------------------------------------------------------------------------------- /components/ui-ux/Alert.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | onClose: () => void; 6 | } 7 | 8 | const Alert = ({ children, onClose }: Props) => { 9 | return ( 10 |
11 |

{children}

12 | 20 |
21 | ); 22 | }; 23 | 24 | export default Alert; 25 | -------------------------------------------------------------------------------- /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 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /components/ui-ux/Button.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent } from "react"; 2 | 3 | interface Props { 4 | children: string; 5 | color?: 6 | | "btn-primary" 7 | | "btn-secondary" 8 | | "btn-error" 9 | | "btn-warning" 10 | | "btn-success"; 11 | onClick: (e: MouseEvent) => void; 12 | } 13 | 14 | function Button({ children, color, onClick }: Props) { 15 | // const handleClick = (e: MouseEvent) => console.log("Clicked:", e.target); 16 | 17 | return ( 18 | //
19 |
20 | {children} 21 |
22 | ); 23 | } 24 | 25 | export default Button; 26 | -------------------------------------------------------------------------------- /features/products/productSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | searchTerm: "", 5 | categoryTerm: "", 6 | }; 7 | 8 | const productSlice = createSlice({ 9 | name: "product", 10 | initialState, 11 | reducers: { 12 | setSearchTerm: (state, action) => { 13 | state.categoryTerm = ""; 14 | state.searchTerm = action.payload; 15 | }, 16 | setCategoryTerm: (state, action) => { 17 | state.searchTerm = ""; 18 | state.categoryTerm = action.payload; 19 | }, 20 | }, 21 | }); 22 | 23 | export const { setSearchTerm, setCategoryTerm } = productSlice.actions; 24 | export default productSlice.reducer; 25 | -------------------------------------------------------------------------------- /features/users/userSlice.tsx: -------------------------------------------------------------------------------- 1 | import { Filter } from "@/global-interfaces"; 2 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 3 | 4 | const initialState = { 5 | filter: { 6 | type: "search", // default filter type 7 | value: "", // default filter value 8 | }, 9 | }; 10 | 11 | const userSlice = createSlice({ 12 | name: "user", 13 | initialState, 14 | reducers: { 15 | setFilter: (state, action: PayloadAction) => { 16 | state.filter.type = action.payload.type; 17 | state.filter.value = action.payload.value; 18 | }, 19 | }, 20 | }); 21 | 22 | export const { setFilter } = userSlice.actions; 23 | export default userSlice.reducer; 24 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./features/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [ 19 | require("@tailwindcss/typography"), 20 | require("@shrutibalasa/tailwind-grid-auto-fit"), 21 | require("daisyui"), 22 | ], 23 | daisyui: { 24 | themes: ["light", "dark"], 25 | styled: true, 26 | rtl: false, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /features/products/apiProducts.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export const apiProducts = createApi({ 4 | reducerPath: "apiProducts", 5 | baseQuery: fetchBaseQuery({ 6 | baseUrl: "https://dummyjson.com", 7 | }), 8 | endpoints: (builder) => ({ 9 | getProducts: builder.query({ 10 | query: ({ searchTerm, categoryTerm }) => { 11 | if (searchTerm) { 12 | return `products/search?q=${searchTerm}`; 13 | } 14 | if (categoryTerm) { 15 | return `products/category/${categoryTerm}`; 16 | } 17 | return `products?limit=10`; 18 | }, 19 | }), 20 | getCategories: builder.query({ 21 | query: () => `products/categories`, 22 | }), 23 | }), 24 | }); 25 | 26 | export const { useGetProductsQuery, useGetCategoriesQuery } = apiProducts; 27 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useContext } from "react"; 2 | import { Footer, Footerbar, Main, Navbar } from "./layouts"; 3 | import { ThemeContext } from "@/contexts/ThemeContext"; 4 | 5 | interface LayoutProps { 6 | children: ReactNode; 7 | } 8 | 9 | function Layout({ children }: LayoutProps) { 10 | const { theme } = useContext(ThemeContext); 11 | 12 | return ( 13 | <> 14 |
15 | 16 |
17 | {children 18 | ? children 19 | : "This is a Layout container. Must have children"} 20 |
21 |
22 | 23 |
24 |
25 | 26 | ); 27 | } 28 | 29 | export default Layout; 30 | -------------------------------------------------------------------------------- /components/ui-ux/ProductSearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { setSearchTerm } from "@/features/products/productSlice"; 3 | import { RootState } from "@/global-interfaces"; 4 | 5 | export const ProductSearchBar = () => { 6 | const dispatch = useDispatch(); 7 | const searchTerm = useSelector( 8 | (state: RootState) => state.product.searchTerm 9 | ); 10 | 11 | const handleInputChange = (event: React.ChangeEvent) => { 12 | dispatch(setSearchTerm(event.target.value)); 13 | }; 14 | 15 | return ( 16 |
17 |
18 | 25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /global-interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface RootState { 2 | product: { 3 | searchTerm: string; 4 | categoryTerm: string; 5 | }; 6 | user: { 7 | filter: Filter; 8 | }; 9 | } 10 | 11 | export interface Filter { 12 | type: "search" | "card" | "currency" | "gender"; 13 | value: string; 14 | } 15 | 16 | export interface ProductsData { 17 | id: number; 18 | title: string; 19 | description: string; 20 | thumbnail: string; 21 | category: string; 22 | brand: string; 23 | } 24 | 25 | export interface User { 26 | id: number; 27 | firstName: string; 28 | lastName: string; 29 | image: string; 30 | gender: string; 31 | bank: Bank; 32 | email: string; 33 | phone: string; 34 | address: Address; 35 | university: string; 36 | username: string; 37 | } 38 | 39 | export interface Bank { 40 | cardType: string; 41 | currency: string; 42 | } 43 | 44 | export interface Address { 45 | city: string; 46 | state: string; 47 | } 48 | -------------------------------------------------------------------------------- /components/layouts/Container.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | className: string; 6 | FULL: boolean; 7 | } 8 | 9 | function Container({ children, className, FULL }: Props) { 10 | return ( 11 | <> 12 | {FULL && ( 13 |
14 | {children 15 | ? children 16 | : "This is a Container container. Must have children"} 17 |
18 | )} 19 | {!FULL && ( 20 |
21 | {children 22 | ? children 23 | : "This is a Container container. Must have children"} 24 |
25 | )} 26 | {/* LG: 1024+ IS SET TO 91% WIDTH (w-11/12) */} 27 | {/* XL: 1280+ IS SET TO 80% WIDTH (w-4/5) */} 28 | 29 | ); 30 | } 31 | 32 | export default Container; 33 | -------------------------------------------------------------------------------- /store/store.ts: -------------------------------------------------------------------------------- 1 | import { apiPost } from "@/features/posts/apiPosts"; 2 | import { apiProducts } from "@/features/products/apiProducts"; 3 | import { apiUsers } from "@/features/users/apiUsers"; 4 | import { configureStore } from "@reduxjs/toolkit"; 5 | import { setupListeners } from "@reduxjs/toolkit/query"; 6 | import productSliceReducer from "@/features/products/productSlice"; 7 | import userSliceReducer from "@/features/users/userSlice"; 8 | 9 | export const store = configureStore({ 10 | reducer: { 11 | [apiPost.reducerPath]: apiPost.reducer, 12 | [apiProducts.reducerPath]: apiProducts.reducer, 13 | [apiUsers.reducerPath]: apiUsers.reducer, 14 | product: productSliceReducer, 15 | user: userSliceReducer, 16 | }, 17 | middleware: (getDefaultMiddleware) => 18 | getDefaultMiddleware().concat([ 19 | apiPost.middleware, 20 | apiProducts.middleware, 21 | apiUsers.middleware, 22 | ]), 23 | }); 24 | 25 | setupListeners(store.dispatch); 26 | -------------------------------------------------------------------------------- /components/ui-ux/UserSearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { setFilter } from "@/features/users/userSlice"; 3 | import { RootState } from "@/global-interfaces"; 4 | 5 | export const UserSearchBar = () => { 6 | const dispatch = useDispatch(); 7 | const filter = useSelector((state: RootState) => state.user.filter); 8 | const searchTerm = filter.type === "search" ? filter.value : ""; 9 | 10 | const handleInputChange = (event: React.ChangeEvent) => { 11 | // For setting a search term 12 | dispatch(setFilter({ type: "search", value: event.target.value })); 13 | }; 14 | 15 | return ( 16 |
17 |
18 | 25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /components/ui-ux/Contact.scss: -------------------------------------------------------------------------------- 1 | form { 2 | display: flex; 3 | align-items: flex-start; 4 | flex-direction: column; 5 | width: 100%; 6 | font-size: 16px; 7 | 8 | input { 9 | width: 100%; 10 | height: 35px; 11 | padding: 7px; 12 | outline: none; 13 | border-radius: 5px; 14 | border: 1px solid rgb(220, 220, 220); 15 | 16 | &:focus { 17 | border: 2px solid rgba(0, 206, 158, 1); 18 | } 19 | } 20 | 21 | textarea { 22 | max-width: 100%; 23 | min-width: 100%; 24 | width: 100%; 25 | max-height: 100px; 26 | min-height: 100px; 27 | padding: 7px; 28 | outline: none; 29 | border-radius: 5px; 30 | border: 1px solid rgb(220, 220, 220); 31 | 32 | &:focus { 33 | border: 2px solid rgba(0, 206, 158, 1); 34 | } 35 | } 36 | 37 | label { 38 | margin-top: 1rem; 39 | } 40 | 41 | input[type='submit'] { 42 | margin-top: 2rem; 43 | cursor: pointer; 44 | background: rgb(249, 105, 14); 45 | color: white; 46 | border: none; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/ui-ux/RadioButtonCategoryGroup.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { setCategoryTerm } from "@/features/products/productSlice"; 3 | import { useGetCategoriesQuery } from "@/features/products/apiProducts"; 4 | 5 | export const RadioButtonCategoryGroup = () => { 6 | const dispatch = useDispatch(); 7 | const { data: categories } = useGetCategoriesQuery({}); 8 | 9 | const handleCategoryChange = (event: React.ChangeEvent) => { 10 | dispatch(setCategoryTerm(event.target.value)); 11 | }; 12 | 13 | return ( 14 |
15 | {categories?.map((category: string[], index: number) => ( 16 | 26 | ))} 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-next13-pages-ts-v1", 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 | "@emailjs/browser": "^3.11.0", 13 | "@heroicons/react": "^2.0.18", 14 | "@reduxjs/toolkit": "^1.9.5", 15 | "@types/node": "20.4.2", 16 | "@types/react": "18.2.15", 17 | "@types/react-dom": "18.2.7", 18 | "autoprefixer": "10.4.14", 19 | "axios": "^1.4.0", 20 | "daisyui": "^3.2.1", 21 | "heroicons": "^2.0.18", 22 | "next": "13.4.10", 23 | "next-redux-wrapper": "^8.1.0", 24 | "postcss": "8.4.26", 25 | "qs": "^6.11.2", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "react-icons": "^4.10.1", 29 | "react-redux": "^8.1.1", 30 | "redux": "^4.2.1", 31 | "sass": "^1.63.6", 32 | "tailwindcss": "3.3.3", 33 | "typescript": "5.1.6" 34 | }, 35 | "devDependencies": { 36 | "@shrutibalasa/tailwind-grid-auto-fit": "^1.1.0", 37 | "@tailwindcss/typography": "^0.5.9", 38 | "@types/qs": "^6.9.7" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /features/users/apiUsers.ts: -------------------------------------------------------------------------------- 1 | import { Filter } from "@/global-interfaces"; 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 3 | 4 | const dataSelect = 5 | "select=firstName,lastName,image,gender,bank,university,address,university,username"; 6 | 7 | export const apiUsers = createApi({ 8 | reducerPath: "apiUsers", 9 | baseQuery: fetchBaseQuery({ 10 | baseUrl: "https://dummyjson.com", 11 | }), 12 | endpoints: (builder) => ({ 13 | getUsers: builder.query({ 14 | query: ({ filter }: { filter: Filter }) => { 15 | switch (filter.type) { 16 | case "search": 17 | return `users/search?q=${filter.value}`; 18 | case "card": 19 | return `users/filter?key=bank.cardType&value=${filter.value}&${dataSelect}`; 20 | case "currency": 21 | return `users/filter?key=bank.currency&value=${filter.value}&${dataSelect}`; 22 | case "gender": 23 | return `users/filter?key=gender&value=${filter.value}&${dataSelect}`; 24 | default: 25 | return `users?limit=8&${dataSelect}`; 26 | } 27 | }, 28 | }), 29 | }), 30 | }); 31 | 32 | export const { useGetUsersQuery } = apiUsers; 33 | -------------------------------------------------------------------------------- /archive/userSlice-1-keep.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | searchTerm: "", 5 | cardTerm: "", 6 | currencyTerm: "", 7 | genderTerm: "", 8 | }; 9 | 10 | const userSlice = createSlice({ 11 | name: "user", 12 | initialState, 13 | reducers: { 14 | setSearchTerm: (state, action) => { 15 | state.cardTerm = ""; 16 | state.currencyTerm = ""; 17 | state.genderTerm = ""; 18 | state.searchTerm = action.payload; 19 | }, 20 | setCardTerm: (state, action) => { 21 | state.currencyTerm = ""; 22 | state.genderTerm = ""; 23 | state.searchTerm = ""; 24 | state.cardTerm = action.payload; 25 | }, 26 | setCurrencyTerm: (state, action) => { 27 | state.genderTerm = ""; 28 | state.searchTerm = ""; 29 | state.cardTerm = ""; 30 | state.currencyTerm = action.payload; 31 | }, 32 | setGenderTerm: (state, action) => { 33 | state.searchTerm = ""; 34 | state.cardTerm = ""; 35 | state.currencyTerm = ""; 36 | state.genderTerm = action.payload; 37 | }, 38 | }, 39 | }); 40 | 41 | export const { setSearchTerm, setCardTerm, setCurrencyTerm, setGenderTerm } = 42 | userSlice.actions; 43 | export default userSlice.reducer; 44 | -------------------------------------------------------------------------------- /components/ui-ux/RadioGroupGender.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { setFilter } from "@/features/users/userSlice"; 3 | 4 | export const RadioGroupGender = () => { 5 | const dispatch = useDispatch(); 6 | const genders = [ 7 | { 8 | id: 1, 9 | name: "Male", 10 | slug: "male", 11 | }, 12 | { 13 | id: 2, 14 | name: "Female", 15 | slug: "female", 16 | }, 17 | ]; 18 | const handleGenderChange = (event: React.ChangeEvent) => { 19 | console.log("GenderTerm in Radio:", event.target.value); 20 | 21 | // For setting a gender term 22 | dispatch(setFilter({ type: "gender", value: event.target.value })); 23 | }; 24 | 25 | return ( 26 |
27 |

Filter by Gender:

28 | {genders?.map((gender) => ( 29 | 39 | ))} 40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /components/ui-ux/ListGroup.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent, useState } from "react"; 2 | import "./ListGroup.scss"; 3 | // import "bootstrap/dist/css/bootstrap.css"; 4 | 5 | interface Props { 6 | items: string[]; 7 | heading: string; 8 | onSelectItem: (item: string) => void; 9 | } 10 | 11 | function ListGroup({ items, heading, onSelectItem }: Props) { 12 | const [selectedIndex, setSelectedIndex] = useState(0); 13 | 14 | // items = []; 15 | 16 | // Bootstrap like classes added for the sake of mosh 17 | return ( 18 | <> 19 |

{heading}

20 | {!items.length && ( 21 | <> 22 |

No items found

23 | 24 | )} 25 |
    26 | {items.map((city, index) => ( 27 |
  • { 35 | setSelectedIndex(index); 36 | onSelectItem(city); 37 | }} 38 | > 39 | {city} 40 |
  • 41 | ))} 42 |
43 | 44 | ); 45 | } 46 | 47 | export default ListGroup; 48 | -------------------------------------------------------------------------------- /components/ui-ux/Contact.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, FormEvent } from "react"; 2 | import emailjs from "@emailjs/browser"; 3 | import "./Contact.scss"; 4 | 5 | // npm i @emailjs/browser 6 | 7 | const Contact = () => { 8 | const form = useRef(null); 9 | 10 | const sendEmail = (e: FormEvent) => { 11 | e.preventDefault(); 12 | 13 | if (form.current !== null) { 14 | emailjs 15 | .sendForm( 16 | "service_yfdtacg", // SERVICE ID 17 | "template_r7i0nqw", // TEMPLATE ID 18 | form.current, 19 | "WGdP1_0dhm0s_-PSD" // PUBLIC KEY 20 | ) 21 | .then( 22 | (result) => { 23 | console.log(result.text); 24 | console.log("message sent"); 25 | }, 26 | (error) => { 27 | console.log(error.text); 28 | } 29 | ); 30 | } 31 | }; 32 | 33 | return ( 34 | <> 35 |
36 | 37 | 38 | 39 | 40 | 41 |