├── .env
├── .gitignore
├── README.md
├── app
├── (root)
│ ├── (home)
│ │ ├── loading.tsx
│ │ └── page.tsx
│ └── layout.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
└── studio
│ └── [[...index]]
│ └── page.tsx
├── components.json
├── components
├── Filters.tsx
├── Footer.tsx
├── Header.tsx
├── Navbar.tsx
├── ResourceCard.tsx
├── SearchForm.tsx
└── ui
│ ├── button.tsx
│ ├── card.tsx
│ ├── input.tsx
│ └── skeleton.tsx
├── lib
└── utils.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── ChatGPT.png
├── Threejs-Cheatsheet.png
├── arrow-blue.svg
├── arrow_trail.svg
├── arrow_white.svg
├── downloads.svg
├── front-end-roadmap.png
├── hamburger-menu.svg
├── jsm-logo.svg
├── jsm_resources_banner.svg
├── jsm_resources_banner.webp
├── magnifying-glass.svg
├── next.svg
├── vercel.svg
└── web3.0.png
├── sanity.cli.ts
├── sanity.config.ts
├── sanity
├── actions.ts
├── env.ts
├── lib
│ ├── client.ts
│ └── image.ts
├── schemas
│ ├── index.ts
│ ├── resource-playlist.schema.ts
│ └── resource.schema.ts
└── utils.ts
├── tailwind.config.ts
└── tsconfig.json
/.env:
--------------------------------------------------------------------------------
1 | # Warning: Do not add secrets (API keys and similar) to this file, as it source controlled!
2 | # Use `.env.local` for any secrets, and ensure it is not added to source control
3 |
4 | NEXT_PUBLIC_SANITY_PROJECT_ID="opkt8022"
5 | NEXT_PUBLIC_SANITY_DATASET="production"
6 | NEXT_PUBLIC_SANITY_TOKEN="skvvPEq1cYFgRgEx8MCFfk5fniYhysg4LmgaEsDSvQdcCZSjeeKrwL908FVJpNUEslX09SlRwcZVKkrgVyy9q5XEmQpYaMhJPODpcOYc2fJYUQ923iSzJISGX0N5bj8ZfLnSUdMI29nTSdTpmQSlTlzRbKJ2FqEdYNP7DXH0BpibRqrkuriw"
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # How I Made My Website Load in 0.364 Seconds | Build and Deploy
2 | 
3 |
4 | ## https://jsmastery.pro/resources
5 |
6 |
7 | # Technologies and Frameworks
8 |
9 | - **Next.js**: A React framework for building JavaScript applications.
10 | - **Sanity.io**: A platform for structured content that comes with an open-source editing environment called Sanity Studio.
11 | - **Tailwind CSS**: A utility-first CSS framework for rapidly building custom user interfaces.
12 | - **TypeScript**: A statically typed superset of JavaScript that adds optional types.
13 | - **React**: A JavaScript library for building user interfaces.
14 | - **Node.js**: A JavaScript runtime built on Chrome's V8 JavaScript engine.
15 | - **CSS**: A stylesheet language used for describing the look and formatting of a document written in HTML.
16 | - **JSON**: A lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse and generate.
17 | - **@sanity/image-url**: A library for generating image URLs with Sanity.io.
18 | - **next-sanity**: A library that provides utilities for working with Sanity.io in Next.js applications.
19 | - **clsx**: A tiny utility for constructing `className` strings conditionally.
20 | - **tailwind-merge**: A utility for merging Tailwind CSS classes.
21 | - **query-string**: A library for parsing and stringifying URL query strings.
22 | # Installation
23 |
24 | Follow these steps to install and setup the project:
25 |
26 | 1. Clone the repository to your local machine using the following command:
27 |
28 | ```bash
29 | git clone https://github.com/adrianhajdin/jsm_resources_next13.git
30 | ```
31 |
32 | 2. Navigate to the project directory:
33 |
34 | ```bash
35 | cd jsm_resources_next13
36 | ```
37 |
38 | 3. Install the required dependencies. The project requires Next.js, React, React DOM, Sanity, Styled Components, Tailwind CSS, TypeScript, and various other dependencies. You can install these using npm or yarn. Here is an example using npm:
39 |
40 | ```bash
41 | npm install
42 | ```
43 |
44 | 4. Run the repo
45 | ```bash
46 | npm run dev
47 | ```
--------------------------------------------------------------------------------
/app/(root)/(home)/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton"
2 |
3 |
4 | const loading = () => {
5 | return (
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default loading
--------------------------------------------------------------------------------
/app/(root)/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | import Filters from '@/components/Filters'
2 | import Header from '@/components/Header';
3 | import ResourceCard from '@/components/ResourceCard'
4 | import SearchForm from '@/components/SearchForm'
5 | import { getResources, getResourcesPlaylist } from '@/sanity/actions'
6 |
7 | export const revalidate = 900;
8 |
9 | interface Props {
10 | searchParams: { [key: string]: string | undefined }
11 | }
12 |
13 | const Page = async ({ searchParams }: Props) => {
14 | const resources = await getResources({
15 | query: searchParams?.query || '',
16 | category: searchParams?.category || '',
17 | page: '1'
18 | })
19 |
20 | const resourcesPlaylist = await getResourcesPlaylist();
21 |
22 | console.log(resourcesPlaylist)
23 |
24 | return (
25 |
26 |
27 |
28 |
JavaScript Mastery Resources
29 |
30 |
31 |
32 |
33 |
34 |
35 | {(searchParams?.query || searchParams?.category) && (
36 |
37 |
41 |
42 |
43 | {resources?.length > 0 ? (
44 | resources.map((resource: any) => (
45 |
53 | ))
54 | ): (
55 |
56 | No resources found
57 |
58 | )}
59 |
60 |
61 | )}
62 |
63 | {resourcesPlaylist.map((item: any) => (
64 |
65 | {item.title}
66 |
67 | {item.resources.map((resource: any) => (
68 |
76 | ))}
77 |
78 |
79 | ))}
80 |
81 | )
82 | }
83 |
84 | export default Page
--------------------------------------------------------------------------------
/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Footer from '@/components/Footer'
4 | import Navbar from '@/components/Navbar'
5 |
6 | const layout = ({ children }: { children: React.ReactNode }) => {
7 | return (
8 | <>
9 |
10 | {children}
11 |
12 | >
13 | )
14 | }
15 |
16 | export default layout
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsm_resources_next13/543c6bb5cd1a6bb2b7cd72ddd0866e19a45ea21b/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400&display=swap");
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | * {
8 | font-family: "Poppins", sans-serif;
9 | }
10 |
11 | @layer utilities {
12 | .paddle-checkout {
13 | @apply min-h-screen w-full py-10 md:py-20 lg:w-1/2;
14 | }
15 |
16 | .hero-height {
17 | @apply min-h-[calc(100vh-100px)];
18 | }
19 |
20 | .text-gradient {
21 | background: linear-gradient(90deg, #4ca5ff 2.34%, #b673f8 100.78%);
22 | -webkit-background-clip: text;
23 | -webkit-text-fill-color: transparent;
24 | background-clip: text;
25 | text-fill-color: transparent;
26 | }
27 |
28 | .heading1 {
29 | @apply text-[64px] leading-[67.2px] font-bold;
30 | }
31 |
32 | .heading2 {
33 | @apply font-bold text-[48px] leading-[50.4px];
34 | }
35 |
36 | .heading3 {
37 | @apply font-bold sm:text-[32px] sm:leading-[33.6px] text-[28px] leading-[40px] tracking-[-0.25%];
38 | }
39 |
40 | .base-regular {
41 | @apply text-[24px] font-normal leading-[31.2px];
42 | }
43 |
44 | .base-bold {
45 | @apply text-[24px] font-semibold leading-[31.2px];
46 | }
47 |
48 | .paragraph-regular {
49 | @apply text-[20px] font-normal leading-[26px];
50 | }
51 |
52 | .paragraph-semibold {
53 | @apply text-[20px] font-semibold leading-[26px];
54 | }
55 |
56 | .body-regular {
57 | @apply text-[16px] font-normal leading-[20.8px];
58 | }
59 |
60 | .body-semibold {
61 | @apply text-[16px] font-semibold leading-[20.8px];
62 | }
63 |
64 | .body-medium {
65 | @apply text-[16px] font-medium leading-[22.4px];
66 | }
67 |
68 | .small-regular {
69 | @apply text-[14px] font-normal leading-[17.5px];
70 | }
71 |
72 | .small-bold {
73 | @apply text-[14px] font-semibold leading-[17.5px];
74 | }
75 |
76 | .heading4 {
77 | @apply font-semibold text-[20px] leading-[26px] tracking-[0.25%];
78 | }
79 |
80 | .body-text {
81 | @apply text-[16px] leading-[22px] font-normal;
82 | }
83 |
84 | .nav-padding {
85 | @apply pt-[98px];
86 | }
87 |
88 | .paddings {
89 | @apply sm:p-16 xs:p-8 px-6 py-12;
90 | }
91 |
92 | .y-paddings {
93 | @apply sm:py-16 py-12;
94 | }
95 |
96 | .x-paddings {
97 | @apply sm:px-16 px-6;
98 | }
99 |
100 | .career-paddings {
101 | @apply sm:p-28 xs:p-8 px-6 py-12;
102 | }
103 |
104 | .flex-between {
105 | @apply flex justify-between items-center;
106 | }
107 |
108 | .flex-center {
109 | @apply flex justify-center items-center;
110 | }
111 |
112 | .flex-start {
113 | @apply flex justify-start items-start;
114 | }
115 |
116 | .flex-end {
117 | @apply flex justify-end;
118 | }
119 |
120 | .inner-width {
121 | @apply 3xl:max-w-[1280px] w-full mx-auto;
122 | }
123 |
124 | .inter-width {
125 | @apply lg:w-[80%] w-[100%];
126 | }
127 |
128 | .no-focus {
129 | @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important;
130 | }
131 | }
132 |
133 | .frame {
134 | border-style: inset;
135 | border: 1px solid #40a0ff;
136 | position: relative;
137 | }
138 |
139 | .sm-box {
140 | width: 25px;
141 | height: 25px;
142 | position: absolute;
143 | border: 1px solid #40a0ff;
144 | }
145 |
146 | .sm-box-1 {
147 | top: -15px;
148 | left: -15px;
149 | }
150 |
151 | .sm-box-2 {
152 | top: -15px;
153 | right: -15px;
154 | }
155 |
156 | .sm-box-3 {
157 | bottom: -15px;
158 | right: -15px;
159 | }
160 |
161 | .sm-box-4 {
162 | bottom: -15px;
163 | left: -15px;
164 | }
165 |
166 | .sm-box-5 {
167 | bottom: 50%;
168 | left: -15px;
169 | }
170 |
171 | .sm-box-6 {
172 | bottom: 50%;
173 | right: -15px;
174 | }
175 |
176 | .sm-box-7 {
177 | bottom: -15px;
178 | left: 50%;
179 | }
180 |
181 | .sm-box-8 {
182 | top: -15px;
183 | left: 50%;
184 | }
185 |
186 | @media screen and (max-width: 500px) {
187 | .sm-box-5,
188 | .sm-box-6,
189 | .sm-box-7,
190 | .sm-box-8 {
191 | display: none;
192 | }
193 |
194 | .frame {
195 | border-radius: 10px;
196 | }
197 | }
198 |
199 | .text-gradient_purple-blue {
200 | background: linear-gradient(90deg, #4c73ff 0%, #73e0f8 100%);
201 | -webkit-background-clip: text;
202 | -webkit-text-fill-color: transparent;
203 | background-clip: text;
204 | text-fill-color: transparent;
205 | }
206 |
207 | .gradient_blue-purple {
208 | background: linear-gradient(90deg, #4ca5ff 0%, #b573f8 100%);
209 | }
210 |
211 | .text-gradient_blue {
212 | background: linear-gradient(90deg, #4c73ff 2.34%, #389bff 100.78%);
213 | -webkit-background-clip: text;
214 | -webkit-text-fill-color: transparent;
215 | background-clip: text;
216 | text-fill-color: transparent;
217 | }
218 |
219 | .gradient_purple {
220 | background: linear-gradient(90deg, #854cff 0%, #b573f8 100%);
221 | }
222 |
223 | .text-gradient_blue-purple {
224 | background: linear-gradient(90deg, #4ca5ff 0%, #b573f8 100%);
225 | -webkit-background-clip: text;
226 | -webkit-text-fill-color: transparent;
227 | background-clip: text;
228 | text-fill-color: transparent;
229 | }
230 |
231 | /* Hide scrollbar for Chrome, Safari and Opera */
232 | .no-scrollbar::-webkit-scrollbar {
233 | display: none;
234 | }
235 |
236 | /* Hide scrollbar for IE, Edge and Firefox */
237 | .no-scrollbar {
238 | -ms-overflow-style: none; /* IE and Edge */
239 | scrollbar-width: none; /* Firefox */
240 | }
241 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import type { Metadata } from 'next'
3 |
4 | export const metadata: Metadata = {
5 | title: 'JS Mastery',
6 | description: 'JS Mastery Resources',
7 | other: {
8 | 'theme-color': '#0d1117',
9 | "color-scheme": "dark only",
10 | "twitter:image": 'https://i.ibb.co/d6TXxB2/homepage-thumbnail.jpg',
11 | "twitter:card": "summary_large_image",
12 | "og:url": "jsmastery.pro",
13 | "og:image": 'https://i.ibb.co/d6TXxB2/homepage-thumbnail.jpg',
14 | "og:type": "website",
15 | }
16 | }
17 |
18 | export default function RootLayout({
19 | children,
20 | }: {
21 | children: React.ReactNode
22 | }) {
23 | return (
24 |
25 | {children}
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/app/studio/[[...index]]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | /**
4 | * This route is responsible for the built-in authoring environment using Sanity Studio.
5 | * All routes under your studio path is handled by this file using Next.js' catch-all routes:
6 | * https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes
7 | *
8 | * You can learn more about the next-sanity package here:
9 | * https://github.com/sanity-io/next-sanity
10 | */
11 |
12 | import { NextStudio } from 'next-sanity/studio'
13 | import config from '../../../sanity.config'
14 |
15 | export default function StudioPage() {
16 | return
17 | }
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/Filters.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { formUrlQuery } from '@/sanity/utils';
4 | import { useState } from 'react'
5 | import { useSearchParams, useRouter } from 'next/navigation'
6 |
7 | const links = ['all', 'Next 13', 'frontend', 'backend', 'fullstack']
8 |
9 | const Filters = () => {
10 | const [active, setActive] = useState('');
11 | const searchParms = useSearchParams();
12 | const router = useRouter();
13 |
14 | const handleFilter = (link: string) => {
15 | let newUrl = '';
16 |
17 | if(active === link) {
18 | setActive('');
19 |
20 | newUrl = formUrlQuery({
21 | params: searchParms.toString(),
22 | keysToRemove: ['category'],
23 | })
24 | } else {
25 | setActive(link);
26 |
27 | newUrl = formUrlQuery({
28 | params: searchParms.toString(),
29 | key: 'category',
30 | value: link.toLowerCase(),
31 | })
32 | }
33 |
34 | router.push(newUrl, { scroll: false });
35 | }
36 |
37 | return (
38 |
39 | {links.map((link) => (
40 |
49 | ))}
50 |
51 | )
52 | }
53 |
54 | export default Filters
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | const Footer = () => {
4 | return (
5 |
13 | )
14 | }
15 |
16 | export default Footer
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | query: string;
3 | category: string;
4 | }
5 |
6 | const Header = ({ query, category }: Props) => {
7 | if(query && category) {
8 | return (
9 |
10 | Search results for "{query}" in {category}
11 |
12 | )
13 | }
14 |
15 | if(query) {
16 | return (
17 |
18 | Search results for "{query}"
19 |
20 | )
21 | }
22 |
23 | if(category) {
24 | return (
25 |
26 | {category}
27 |
28 | )
29 | }
30 |
31 | return (
32 | No Results
33 | )
34 | }
35 |
36 | export default Header
--------------------------------------------------------------------------------
/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import Link from 'next/link'
3 |
4 | const Navbar = () => {
5 | return (
6 |
40 | )
41 | }
42 |
43 | export default Navbar
--------------------------------------------------------------------------------
/components/ResourceCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
5 |
6 | interface Props {
7 | id: string;
8 | title: string;
9 | image: string;
10 | downloadNumber: number;
11 | downloadLink: string;
12 | }
13 |
14 | const ResourceCard = ({ id, title, image, downloadNumber, downloadLink }: Props) => {
15 | return (
16 |
17 |
18 |
19 |
20 |
27 |
28 | {title}
29 |
30 |
31 |
32 |
33 |
36 | {downloadNumber}
37 |
38 |
39 | Download Now
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | export default ResourceCard
--------------------------------------------------------------------------------
/components/SearchForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4 | import { useState, useEffect } from 'react';
5 | import Image from 'next/image'
6 |
7 | import { Input } from "@/components/ui/input"
8 | import { formUrlQuery } from '@/sanity/utils';
9 |
10 | const SearchForm = () => {
11 | const searchParams = useSearchParams();
12 | const router = useRouter();
13 | const pathname = usePathname();
14 |
15 | const [search, setSearch] = useState('');
16 |
17 | useEffect(() => {
18 | const delayDebounceFn = setTimeout(() => {
19 | let newUrl = '';
20 |
21 | if(search) {
22 | newUrl = formUrlQuery({
23 | params: searchParams.toString(),
24 | key: 'query',
25 | value: search
26 | })
27 | } else {
28 | newUrl = formUrlQuery({
29 | params: searchParams.toString(),
30 | keysToRemove: ['query']
31 | })
32 | }
33 |
34 | router.push(newUrl, { scroll: false });
35 | }, 300)
36 |
37 | return () => clearTimeout(delayDebounceFn)
38 | }, [search])
39 |
40 |
41 | return (
42 |
60 | )
61 | }
62 |
63 | export default SearchForm
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ['cdn.sanity.io']
5 | }
6 | }
7 |
8 | module.exports = nextConfig
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs_jsmpro",
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 | "@radix-ui/react-slot": "^1.0.2",
13 | "@sanity/image-url": "^1.0.2",
14 | "@sanity/vision": "^3.16.4",
15 | "@types/node": "20.6.0",
16 | "@types/react": "18.2.21",
17 | "@types/react-dom": "18.2.7",
18 | "autoprefixer": "10.4.15",
19 | "class-variance-authority": "^0.7.0",
20 | "clsx": "^2.0.0",
21 | "lucide-react": "^0.276.0",
22 | "next": "13.4.19",
23 | "next-sanity": "^5.4.6",
24 | "postcss": "8.4.29",
25 | "query-string": "^8.1.0",
26 | "react": "18.2.0",
27 | "react-dom": "18.2.0",
28 | "sanity": "^3.16.4",
29 | "styled-components": "5.2",
30 | "tailwind-merge": "^1.14.0",
31 | "tailwindcss": "3.3.3",
32 | "tailwindcss-animate": "^1.0.7",
33 | "typescript": "5.2.2"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/ChatGPT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsm_resources_next13/543c6bb5cd1a6bb2b7cd72ddd0866e19a45ea21b/public/ChatGPT.png
--------------------------------------------------------------------------------
/public/Threejs-Cheatsheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsm_resources_next13/543c6bb5cd1a6bb2b7cd72ddd0866e19a45ea21b/public/Threejs-Cheatsheet.png
--------------------------------------------------------------------------------
/public/arrow-blue.svg:
--------------------------------------------------------------------------------
1 |
10 |
11 |
--------------------------------------------------------------------------------
/public/arrow_trail.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/arrow_white.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/downloads.svg:
--------------------------------------------------------------------------------
1 |
10 |
11 |
--------------------------------------------------------------------------------
/public/front-end-roadmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsm_resources_next13/543c6bb5cd1a6bb2b7cd72ddd0866e19a45ea21b/public/front-end-roadmap.png
--------------------------------------------------------------------------------
/public/hamburger-menu.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/jsm-logo.svg:
--------------------------------------------------------------------------------
1 |
16 |
17 |
--------------------------------------------------------------------------------
/public/jsm_resources_banner.svg:
--------------------------------------------------------------------------------
1 |
94 |
--------------------------------------------------------------------------------
/public/jsm_resources_banner.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsm_resources_next13/543c6bb5cd1a6bb2b7cd72ddd0866e19a45ea21b/public/jsm_resources_banner.webp
--------------------------------------------------------------------------------
/public/magnifying-glass.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/web3.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/jsm_resources_next13/543c6bb5cd1a6bb2b7cd72ddd0866e19a45ea21b/public/web3.0.png
--------------------------------------------------------------------------------
/sanity.cli.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This configuration file lets you run `$ sanity [command]` in this folder
3 | * Go to https://www.sanity.io/docs/cli to learn more.
4 | **/
5 | import { defineCliConfig } from 'sanity/cli'
6 |
7 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
8 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
9 |
10 | export default defineCliConfig({ api: { projectId, dataset } })
11 |
--------------------------------------------------------------------------------
/sanity.config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This configuration is used to for the Sanity Studio that’s mounted on the `/app/studio/[[...index]]/page.tsx` route
3 | */
4 |
5 | import { visionTool } from "@sanity/vision";
6 | import { defineConfig } from "sanity";
7 | import { deskTool } from "sanity/desk";
8 |
9 | // Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works
10 | import { apiVersion, dataset, projectId } from "./sanity/env";
11 | import schemas from "./sanity/schemas";
12 |
13 | export default defineConfig({
14 | basePath: "/studio",
15 | projectId,
16 | dataset,
17 | // Add and edit the content schema in the './sanity/schema' folder
18 | schema: { types: schemas },
19 | plugins: [
20 | deskTool(),
21 | // Vision is a tool that lets you query your content with GROQ in the studio
22 | // https://www.sanity.io/docs/the-vision-plugin
23 | visionTool({ defaultApiVersion: apiVersion }),
24 | ],
25 | });
26 |
--------------------------------------------------------------------------------
/sanity/actions.ts:
--------------------------------------------------------------------------------
1 | import { groq } from 'next-sanity';
2 | import { readClient } from './lib/client';
3 | import { buildQuery } from './utils';
4 |
5 | interface GetResourcesParams {
6 | query: string;
7 | category: string;
8 | page: string;
9 | }
10 |
11 | export const getResourcesPlaylist = async () => {
12 | try {
13 | const resources = await readClient.fetch(
14 | groq`*[_type == "resourcePlaylist"]{
15 | _id,
16 | title,
17 | resources[0...6]->{
18 | title,
19 | _id,
20 | downloadLink,
21 | "image": poster.asset->url,
22 | views,
23 | category
24 | }
25 | }`
26 | );
27 |
28 | return resources;
29 | } catch (error) {
30 | console.log(error);
31 | }
32 | }
33 |
34 | export const getResources = async (params: GetResourcesParams) => {
35 | const { query, category, page } = params;
36 |
37 | try {
38 | const resources = await readClient.fetch(
39 | groq`${buildQuery({
40 | type: 'resource',
41 | query,
42 | category,
43 | page: parseInt(page),
44 | })}{
45 | title,
46 | _id,
47 | downloadLink,
48 | "image": poster.asset->url,
49 | views,
50 | slug,
51 | category
52 | }`
53 | );
54 |
55 | return resources;
56 | } catch (error) {
57 | console.log(error);
58 | }
59 | }
--------------------------------------------------------------------------------
/sanity/env.ts:
--------------------------------------------------------------------------------
1 | export const apiVersion =
2 | process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-09-13'
3 |
4 | export const dataset = assertValue(
5 | process.env.NEXT_PUBLIC_SANITY_DATASET,
6 | 'Missing environment variable: NEXT_PUBLIC_SANITY_DATASET'
7 | )
8 |
9 | export const projectId = assertValue(
10 | process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
11 | 'Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID'
12 | )
13 |
14 | export const token = assertValue(
15 | process.env.NEXT_PUBLIC_SANITY_TOKEN,
16 | 'Missing environment variable: NEXT_PUBLIC_SANITY_TOKEN'
17 | )
18 |
19 | export const useCdn = false
20 |
21 | function assertValue(v: T | undefined, errorMessage: string): T {
22 | if (v === undefined) {
23 | throw new Error(errorMessage)
24 | }
25 |
26 | return v
27 | }
28 |
--------------------------------------------------------------------------------
/sanity/lib/client.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from 'next-sanity'
2 |
3 | import { apiVersion, dataset, projectId, useCdn, token } from '../env'
4 |
5 | export const readClient = createClient({
6 | apiVersion,
7 | dataset,
8 | projectId,
9 | useCdn,
10 | })
11 |
12 | export const writeClient = createClient({
13 | apiVersion,
14 | dataset,
15 | projectId,
16 | useCdn,
17 | token,
18 | })
19 |
--------------------------------------------------------------------------------
/sanity/lib/image.ts:
--------------------------------------------------------------------------------
1 | import createImageUrlBuilder from '@sanity/image-url'
2 | import type { Image } from 'sanity'
3 |
4 | import { dataset, projectId } from '../env'
5 |
6 | const imageBuilder = createImageUrlBuilder({
7 | projectId: projectId || '',
8 | dataset: dataset || '',
9 | })
10 |
11 | export const urlForImage = (source: Image) => {
12 | return imageBuilder?.image(source).auto('format').fit('max')
13 | }
14 |
--------------------------------------------------------------------------------
/sanity/schemas/index.ts:
--------------------------------------------------------------------------------
1 | import resource from './resource.schema';
2 | import resourcePlaylist from './resource-playlist.schema';
3 |
4 | const schemas = [resource, resourcePlaylist]
5 |
6 | export default schemas;
--------------------------------------------------------------------------------
/sanity/schemas/resource-playlist.schema.ts:
--------------------------------------------------------------------------------
1 | const schema = {
2 | name: "resourcePlaylist",
3 | title: "Resource Playlist",
4 | type: "document",
5 | fields: [
6 | {
7 | name: "title",
8 | title: "Title",
9 | type: "string",
10 | validation: (Rule: any) => Rule.required(),
11 | },
12 | {
13 | name: "resources",
14 | title: "Resources",
15 | type: "array",
16 | of: [
17 | {
18 | type: "reference",
19 | to: [{ type: "resource" }],
20 | },
21 | ],
22 | },
23 | ],
24 | };
25 |
26 | export default schema;
27 |
--------------------------------------------------------------------------------
/sanity/schemas/resource.schema.ts:
--------------------------------------------------------------------------------
1 | const schema = {
2 | name: 'resource',
3 | title: 'Resource',
4 | type: 'document',
5 | fields: [
6 | {
7 | name: 'title',
8 | title: 'Title',
9 | type: 'string',
10 | require,
11 | validation: (Rule: any) => Rule.required()
12 | },
13 | {
14 | name: 'slug',
15 | title: 'Slug',
16 | type: 'slug',
17 | options: { source: 'title' }
18 | },
19 | {
20 | name: 'downloadLink',
21 | title: 'Download Link',
22 | type: 'url',
23 | validation: (Rule: any) => Rule.required()
24 | },
25 | {
26 | name: 'views',
27 | title: 'Views',
28 | type: 'number',
29 | initialValue: 0,
30 | },
31 | {
32 | name: 'poster',
33 | title: 'Poster',
34 | type: 'image',
35 | validation: (Rule: any) => Rule.required(),
36 | options: {
37 | hotspot: true,
38 | }
39 | },
40 | {
41 | name: 'category',
42 | title: 'Category',
43 | type: 'string',
44 | validation: (Rule: any) => Rule.required(),
45 | options: {
46 | list: ['frontend', 'backend', 'next 13', 'fullstack', 'other']
47 | }
48 | }
49 | ]
50 | }
51 |
52 | export default schema;
--------------------------------------------------------------------------------
/sanity/utils.ts:
--------------------------------------------------------------------------------
1 | import qs from 'query-string'
2 |
3 | interface BuildQueryParams {
4 | type: string;
5 | query: string;
6 | category: string;
7 | page: number;
8 | perPage?: number;
9 | }
10 |
11 | export function buildQuery(params: BuildQueryParams) {
12 | const { type, query, category, page = 1, perPage = 20 } = params;
13 |
14 | const conditions = [`*[_type=="${type}"`];
15 |
16 | if (query) conditions.push(`title match "*${query}*"`);
17 |
18 | if (category && category !== "all") {
19 | conditions.push(`category == "${category}"`);
20 | }
21 |
22 | // Calculate pagination limits
23 | const offset = (page - 1) * perPage;
24 | const limit = perPage;
25 |
26 | return conditions.length > 1
27 | ? `${conditions[0]} && (${conditions
28 | .slice(1)
29 | .join(" && ")})][${offset}...${limit}]`
30 | : `${conditions[0]}][${offset}...${limit}]`;
31 | }
32 |
33 | interface UrlQueryParams {
34 | params: string;
35 | key?: string;
36 | value?: string | null;
37 | keysToRemove?: string[];
38 | }
39 |
40 | export function formUrlQuery({ params, key, value, keysToRemove }: UrlQueryParams) {
41 | const currentUrl = qs.parse(params);
42 |
43 | if(keysToRemove) {
44 | keysToRemove.forEach((keyToRemove) => {
45 | delete currentUrl[keyToRemove];
46 | })
47 | } else if(key && value) {
48 | currentUrl[key] = value;
49 | }
50 |
51 | return qs.stringifyUrl(
52 | { url: window.location.pathname, query: currentUrl },
53 | { skipNull: true }
54 | )
55 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{ts,tsx}",
6 | "./components/**/*.{ts,tsx}",
7 | "./app/**/*.{ts,tsx}",
8 | "./src/**/*.{ts,tsx}",
9 | ],
10 | theme: {
11 | screens: {
12 | xs: "400px",
13 | sm: "640px",
14 | md: "768px",
15 | lg: "1024px",
16 | xl: "1220px",
17 | "2xl": "1440px",
18 | "3xl": "1700px",
19 | },
20 | container: {
21 | center: true,
22 | padding: "2rem",
23 | screens: {
24 | "2xl": "1400px",
25 | },
26 | },
27 | extend: {
28 | fontFamily: {
29 | poppins: ["Poppins", "sans-serif"],
30 | inter: ["Inter", "sans-serif"],
31 | },
32 | colors: {
33 | primary: "#2190FF",
34 | black: {
35 | DEFAULT: "#000",
36 | 100: "#0D1117",
37 | 200: "#161B22",
38 | 300: "#1F2428",
39 | 400: "#242C38",
40 | },
41 | grey: {
42 | 100: "#969BA5",
43 | 200: "#55616D",
44 | },
45 | white: {
46 | DEFAULT: "#FFF",
47 | 400: "#A3B3BC",
48 | 500: "#A4B8D5",
49 | 800: "#D0DFFF",
50 | },
51 | purple: "#8C7CFF",
52 | pink: "#ED5FBD",
53 | violet: "#F16565",
54 | orange: "#FF964B",
55 | },
56 | backgroundImage: {
57 | banner: "url('/jsm_resources_banner.svg')",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | };
77 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------