├── .eslintrc.json
├── .github
└── FUNDING.yml
├── .gitignore
├── README.md
├── jsconfig.json
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.svg
├── fonts
│ └── OverusedGrotesk-VF.woff2
├── images
│ └── profile.webp
└── logos
│ ├── 404logo.svg
│ ├── Logofull.svg
│ ├── Logouppercase.svg
│ ├── footer.svg
│ └── logosimple.svg
├── src
├── app
│ ├── about
│ │ └── page.jsx
│ ├── globals.css
│ ├── layout.jsx
│ ├── legal
│ │ └── page.jsx
│ ├── not-found.jsx
│ ├── opengraph-image-alt.txt
│ ├── opengraph-image.png
│ ├── page.jsx
│ ├── resources
│ │ └── [slug]
│ │ │ └── page.jsx
│ ├── robots.jsx
│ ├── sitemap.js
│ ├── twitter-image.alt.txt
│ └── twitter-image.png
└── components
│ ├── Button
│ ├── Button.jsx
│ └── LoadMoreButton.jsx
│ ├── Card
│ ├── ResourceCard.jsx
│ ├── ResourceContainer.jsx
│ └── Skeleton.jsx
│ ├── Footer
│ └── Footer.jsx
│ ├── Header
│ └── Navbar.jsx
│ ├── SVGs
│ ├── FooterTitle.jsx
│ ├── Logofull.jsx
│ └── Logosimple.jsx
│ └── TabNavigation
│ ├── TabButtons.jsx
│ └── TabButtonsMobile.jsx
└── tailwind.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: supporthuyng
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./src/*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | images: {
3 | dangerouslyAllowSVG: true,
4 | remotePatterns: [
5 | {
6 | protocol: 'https',
7 | hostname: 'images.ctfassets.net',
8 | },
9 | {
10 | protocol: 'https',
11 | hostname: 'api.producthunt.com',
12 | },
13 | ],
14 | },
15 | experimental: {
16 | scrollRestoration: true,
17 | },
18 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webstack",
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 | "@tailwindcss/container-queries": "^0.1.1",
13 | "@vercel/analytics": "^1.1.1",
14 | "contentful": "^10.6.5",
15 | "framer-motion": "^10.16.4",
16 | "next": "^14.0.3",
17 | "react": "^18",
18 | "react-copy-to-clipboard": "^5.1.0",
19 | "react-dom": "^18",
20 | "react-icons": "^4.11.0",
21 | "sharp": "^0.32.6"
22 | },
23 | "devDependencies": {
24 | "autoprefixer": "^10.4.17",
25 | "eslint": "^8",
26 | "eslint-config-next": "14.0.1",
27 | "postcss": "^8.4.33",
28 | "prettier": "^3.0.3",
29 | "prettier-plugin-tailwindcss": "^0.5.6",
30 | "tailwindcss": "^3.4.1"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/public/fonts/OverusedGrotesk-VF.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithUsman0/Pillarstack/94f6f97d73aca7ef71fe32df36fbbcf3d7e4b28d/public/fonts/OverusedGrotesk-VF.woff2
--------------------------------------------------------------------------------
/public/images/profile.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithUsman0/Pillarstack/94f6f97d73aca7ef71fe32df36fbbcf3d7e4b28d/public/images/profile.webp
--------------------------------------------------------------------------------
/public/logos/404logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/logos/Logofull.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/logos/Logouppercase.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/logos/footer.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/logos/logosimple.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/about/page.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import profile from "../../../public/images/profile.webp";
4 | import Image from "next/image";
5 |
6 | import { useState } from "react";
7 | import { CopyToClipboard } from "react-copy-to-clipboard";
8 |
9 |
10 | // export const metadata = {
11 | // metadataBase: new URL('https://pillarstack.com'),
12 | // alternates: {
13 | // canonical: '/about',
14 | // languages: {
15 | // 'en-US': '/en-US',
16 | // },
17 | // },
18 | // title: "Pillarstack — About Pillarstack",
19 | // description:
20 | // "Learn more about Pillarstack and the rationale behind its creation.",
21 | // };
22 |
23 | export default function About() {
24 | const [copied, setCopied] = useState(false);
25 |
26 | const handleCopy = () => {
27 | setCopied(true);
28 | setTimeout(() => {
29 | setCopied(false);
30 | }, 2000);
31 | };
32 |
33 |
34 |
35 | return (
36 |
37 |
38 |
39 | About Pillarstack
40 |
41 |
42 |
43 |
44 |
45 | I created Pillarstack to address the frustrations I encountered when
46 | I started out in frontend development and web design. Hunting for
47 | resources and tools consumed a lot valuable time that could have
48 | been better spent honing my skills.{" "}
49 |
50 |
51 |
52 | Now Pillarstack exists to support those that have similar issues.
53 | These resources are handpicked and curated by me and other amazing
54 | contributors.
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
Huy Nguyen
68 |
69 |
70 | {copied ? "Copied to clipboard!" : "hello@huyng.xyz"}
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | *,
7 | *::before,
8 | *::after {
9 | box-sizing: border-box;
10 | }
11 |
12 | * {
13 | /* border: 1px solid red; */
14 | margin: 0;
15 | padding: 0;
16 | font: inherit;
17 | }
18 |
19 | img,
20 | svg {
21 | display: block;
22 | max-width: 100%;
23 | }
24 |
25 | p {
26 | @apply tracking-base;
27 | }
28 |
29 | .section-padding {
30 | @apply px-6 sm:px-[5%];
31 | }
32 |
33 | h1,
34 | h2,
35 | h3,
36 | h4,
37 | h5 {
38 | @apply tracking-close;
39 | }
40 |
41 | ::-moz-selection {
42 | background: #555555;
43 | }
44 | ::-webkit-selection {
45 | background: #555555;
46 | }
47 | ::selection {
48 | background: #555555;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/layout.jsx:
--------------------------------------------------------------------------------
1 | import localFont from "next/font/local";
2 | import "./globals.css";
3 | import Navbar from "@/components/Header/Navbar";
4 | import Footer from "@/components/Footer/Footer";
5 | import { Analytics } from "@vercel/analytics/react";
6 |
7 | const overusedgrotesk = localFont({
8 | src: [
9 | {
10 | path: "../../public/fonts/OverusedGrotesk-VF.woff2",
11 | },
12 | ],
13 | display: "block",
14 | variable: "--font-overusedgrotesk",
15 | });
16 |
17 | export const metadata = {
18 | metadataBase: new URL('https://pillarstack.com'),
19 | alternates: {
20 | canonical: '/',
21 | languages: {
22 | 'en-US': '/en-US',
23 | },
24 | },
25 | title: "Pillarstack — Resources for web developers and designers",
26 | description:
27 | "Assorted resources for frontend developers and web designers. Explore curated and handpicked goodies that enhance your workflow and cultivate your growth.",
28 | };
29 |
30 | export default function RootLayout({ children }) {
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {children}
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/legal/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import profile from "../../../public/images/profile.webp";
4 | import Image from "next/image";
5 |
6 | import { useState } from "react";
7 | import { CopyToClipboard } from "react-copy-to-clipboard";
8 |
9 | // export const metadata = {
10 | // metadataBase: new URL('https://pillarstack.com'),
11 | // alternates: {
12 | // canonical: '/legal',
13 | // languages: {
14 | // 'en-US': '/en-US',
15 | // },
16 | // },
17 | // title: "Pillarstack — Legal Information",
18 | // description:
19 | // "Legal details on trademarks and user privacy on Pillarstack",
20 | // };
21 |
22 | export default function About() {
23 | const [copied, setCopied] = useState(false);
24 |
25 | const handleCopy = () => {
26 | setCopied(true);
27 | setTimeout(() => {
28 | setCopied(false);
29 | }, 2000);
30 | };
31 |
32 | return (
33 |
34 |
35 |
36 | Legal Info
37 |
38 |
39 |
40 |
41 |
42 | This website does not use cookies or any other tracking technology.
43 |
44 |
45 |
46 | All trademarks, logos, brand and company names are the property of
47 | their respective owners.
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 |
Huy Nguyen
61 |
62 |
63 | {copied ? "Copied to clipboard!" : "hello@huyng.xyz"}
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/not-found.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import Button from "@/components/Button/Button";
4 |
5 | export default function ErrorPage() {
6 | return (
7 |
8 |
9 |
16 |
17 |
18 | 404
19 |
20 |
21 | the page you’re looking for does not exist.
22 |
23 |
24 |
Go Back home
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/opengraph-image-alt.txt:
--------------------------------------------------------------------------------
1 | Pillarstack: Assorted resources for frontend developers and web designers.
--------------------------------------------------------------------------------
/src/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithUsman0/Pillarstack/94f6f97d73aca7ef71fe32df36fbbcf3d7e4b28d/src/app/opengraph-image.png
--------------------------------------------------------------------------------
/src/app/page.jsx:
--------------------------------------------------------------------------------
1 | import ResourceContainer from "@/components/Card/ResourceContainer";
2 | import Tab from "@/components/TabNavigation/TabButtons";
3 | import TabMobile from "@/components/TabNavigation/TabButtonsMobile";
4 | import { createClient } from "contentful";
5 | import { Suspense } from "react";
6 | import Skeleton from "@/components/Card/Skeleton";
7 |
8 |
9 | async function fetchCategories() {
10 | const client = createClient({
11 | space: process.env.CONTENTFUL_SPACE_ID,
12 | accessToken: process.env.CONTENTFUL_ACCESS_KEY,
13 | });
14 |
15 | const res = await client.getEntries({
16 | content_type: "resourcesPage",
17 | order: ["fields.title"],
18 | include: 2,
19 | });
20 |
21 | return res.items;
22 | }
23 |
24 | export default async function Home({ searchParams }) {
25 | const { category } = searchParams;
26 | const cat = await fetchCategories();
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | Assorted resources{" "}
35 |
36 | for frontend developers and web designers.
37 |
38 |
39 |
40 | Explore curated and handpicked goodies that enhance your workflow and
41 | cultivate your growth as a developer and designer.
42 |
43 |
44 |
45 |
46 |
47 | }>
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/resources/[slug]/page.jsx:
--------------------------------------------------------------------------------
1 | import Button from "@/components/Button/Button";
2 | import { createClient } from "contentful";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { GoArrowLeft } from "react-icons/go";
6 |
7 | const client = createClient({
8 | space: process.env.CONTENTFUL_SPACE_ID,
9 | accessToken: process.env.CONTENTFUL_ACCESS_KEY,
10 | });
11 |
12 | export async function generateStaticParams() {
13 | const res = await client.getEntries({
14 | content_type: "resourcesPage",
15 | });
16 |
17 | return res.items.map((item) => ({
18 | slug: item.fields.slug,
19 | }));
20 | }
21 |
22 |
23 |
24 | async function fetchResource({ slug }) {
25 | const res = await client.getEntries({
26 | content_type: "resourcesPage",
27 | "fields.slug": slug,
28 | });
29 |
30 | return res.items[0];
31 | }
32 |
33 |
34 |
35 | export default async function ResourceDetails({ params }) {
36 | const resource = await fetchResource(params);
37 |
38 | return (
39 |
40 |
41 |
42 | Back
43 |
44 |
45 |
58 |
59 |
60 |
{resource.fields.title}
61 |
62 | {resource.fields.description}
63 |
64 |
65 | View Source
66 |
67 |
68 |
69 |
70 |
71 |
Category
72 |
73 | {resource.fields.category.fields.category}
74 |
75 |
76 |
77 |
Tags
78 |
79 | {resource.fields.tags.map((tag) => (
80 |
84 | {tag.fields.tag}
85 |
86 | ))}
87 |
88 |
89 |
90 |
91 |
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/src/app/robots.jsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithUsman0/Pillarstack/94f6f97d73aca7ef71fe32df36fbbcf3d7e4b28d/src/app/robots.jsx
--------------------------------------------------------------------------------
/src/app/sitemap.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithUsman0/Pillarstack/94f6f97d73aca7ef71fe32df36fbbcf3d7e4b28d/src/app/sitemap.js
--------------------------------------------------------------------------------
/src/app/twitter-image.alt.txt:
--------------------------------------------------------------------------------
1 | Pillarstack: Assorted resources for frontend developers and web designers.
--------------------------------------------------------------------------------
/src/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithUsman0/Pillarstack/94f6f97d73aca7ef71fe32df36fbbcf3d7e4b28d/src/app/twitter-image.png
--------------------------------------------------------------------------------
/src/components/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { GoArrowRight } from "react-icons/go";
3 |
4 | export default function Button({ children, href, ...props }) {
5 | return (
6 |
11 | {children}
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Button/LoadMoreButton.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { GoArrowDown } from "react-icons/go";
5 |
6 | export default function LoadMoreButton({onClick}) {
7 |
8 |
9 | return (
10 |
14 | Load More
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Card/ResourceCard.jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import { GoArrowUpRight } from "react-icons/go";
4 |
5 | export default function ResourceCard({ resource }) {
6 | const { title, category, thumbnail, tags, slug } = resource.fields;
7 |
8 | // Extract tag names from the tags reference field
9 | const tagNames = tags.map((tag) => tag.fields.tag);
10 | tagNames.sort()
11 |
12 |
13 | return (
14 | console.log(tagNames),
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
{title}
28 |
29 | {tagNames.map((tag, index) => (
30 |
31 | {tag}
32 |
33 | ))}
34 |
35 |
36 |
37 | {category.fields.category}
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Card/ResourceContainer.jsx:
--------------------------------------------------------------------------------
1 | // Library
2 | import { createClient } from "contentful";
3 |
4 | // Components
5 | import ResourceCard from "@/components/Card/ResourceCard";
6 |
7 |
8 | async function fetchContentful(category, skip, limit) {
9 |
10 |
11 | const client = createClient({
12 | space: process.env.CONTENTFUL_SPACE_ID,
13 | accessToken: process.env.CONTENTFUL_ACCESS_KEY,
14 | });
15 |
16 | const res = await client.getEntries({
17 | content_type: "resourcesPage",
18 | include: 2,
19 | skip,
20 | limit,
21 | order: ["fields.title"],
22 | "fields.category.sys.contentType.sys.id": "categories",
23 | "fields.category.fields.category": category === "all" ? null : category,
24 | });
25 |
26 | return res.items;
27 | }
28 |
29 | export default async function ResourceContainer({ category }) {
30 | const resources = await fetchContentful(category);
31 |
32 | return (
33 | <>
34 |
35 | {resources.map((resource) => {
36 | return ;
37 | })}
38 |
39 | >
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Card/Skeleton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function Skeleton() {
4 | return (
5 |
6 |
7 |
12 |
13 |
14 |
15 |
Loading...
16 |
17 |
18 | Loading...
19 |
20 |
21 |
22 |
23 | Loading...
24 |
25 |
26 |
27 | );
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { FooterTitle } from "../SVGs/FooterTitle";
3 | import { GoArrowUpRight } from "react-icons/go";
4 | import Image from "next/image";
5 |
6 | export default function Footer() {
7 | const links = [
8 | {
9 | id: 1,
10 | href: "/about",
11 | label: "About",
12 | },
13 | {
14 | id: 2,
15 | href: "https://forms.gle/PftXkai3sNZquWu68",
16 | label: "Add a resource",
17 | },
18 | {
19 | id: 3,
20 | href: "https://forms.gle/s84TNQcUX1P22bTE7",
21 | label: "Submit feedback",
22 | },
23 | {
24 | id: 4,
25 | href: "https://ko-fi.com/supporthuyng",
26 | label: "Support this project",
27 | },
28 | {
29 | id: 5,
30 | href: "/legal",
31 | label: "Legal",
32 | },
33 | ];
34 |
35 | return (
36 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/Header/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import Button from "../Button/Button";
2 | import { LogoFull } from "../SVGs/Logofull";
3 | import { LogoSimple } from "../SVGs/Logosimple";
4 | import Link from "next/link";
5 |
6 | export default function Navbar() {
7 | return (
8 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/SVGs/FooterTitle.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const FooterTitle = () => {
4 | return (
5 |
10 |
14 |
18 |
22 |
26 |
30 |
34 |
38 |
42 |
46 |
50 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/components/SVGs/Logofull.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const LogoFull = () => {
4 | return (
5 |
13 |
14 |
20 |
21 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/SVGs/Logosimple.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const LogoSimple = () => {
4 | return (
5 |
13 |
14 |
20 |
21 |
22 |
30 |
31 |
32 |
33 |
34 |
40 |
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/TabNavigation/TabButtons.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // Note: This component is used to filter resources by category
4 | import { useRouter, useSearchParams } from "next/navigation";
5 |
6 |
7 | // ... (other imports)
8 |
9 | export default function TabButtons({ cat }) {
10 | const router = useRouter();
11 | const searchParams = useSearchParams();
12 |
13 | const categoryCount = {};
14 | cat.forEach((resource) => {
15 | const categoryItem = resource.fields.category.fields.category;
16 | categoryCount[categoryItem] = (categoryCount[categoryItem] || 0) + 1;
17 | });
18 |
19 | // Extract category from the URL query
20 | const activeCategory = searchParams.get("category") || "";
21 |
22 | return (
23 |
24 | {/* Filtering button for "All" category */}
25 | {
28 | router.push("/", { scroll: false });
29 | }}
30 | className={`py-1 px-4 flex gap-x-1 font-medium items-center border border-dim-gray rounded-full hover:border-text transition-all text-sm xl:text-h6 ${
31 | activeCategory === "" ? "bg-accent text-bg" : " bg-bg text-accent"
32 | }`}
33 | >
34 | All
35 |
36 | {Object.entries(categoryCount).map(([item, count]) => {
37 | return (
38 | {
41 | e.preventDefault();
42 | router.push(`/?category=${item}`, { scroll: false });
43 | }}
44 | className={`py-1 px-4 flex gap-x-1 font-medium border border-dim-gray rounded-full hover:border-text transition-all relative ${
45 | activeCategory === item ? "text-bg bg-accent" : " bg-bg text-accent"
46 | }`}
47 | >
48 | {item}
49 |
50 | {count}
51 |
52 |
53 | );
54 | })}
55 |
56 | );
57 | }
--------------------------------------------------------------------------------
/src/components/TabNavigation/TabButtonsMobile.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // Note: This component is used to filter resources by category
4 | import { useRouter, useSearchParams } from "next/navigation";
5 | import { useState } from "react";
6 | import { GoPlus } from "react-icons/go";
7 |
8 | // ... (other imports)
9 |
10 | export default function TabButtons({ cat }) {
11 | const router = useRouter();
12 | const searchParams = useSearchParams();
13 | const [isOpen, setIsOpen] = useState(false);
14 |
15 | const categoryCount = {};
16 | cat.forEach((resource) => {
17 | const categoryItem = resource.fields.category.fields.category;
18 | categoryCount[categoryItem] = (categoryCount[categoryItem] || 0) + 1;
19 | });
20 |
21 | // Extract category from the URL query
22 | const activeCategory = searchParams.get("category") || "";
23 |
24 | const handleCategoryChange = (category) => {
25 | setIsOpen(false);
26 | router.push(`/?category=${category}`, { scroll: false });
27 | };
28 |
29 | return (
30 |
31 |
setIsOpen(!isOpen)}
33 | className={`py-4 px-4 flex gap-x-1 font-bold border bg-dark-charcoal border-dim-gray rounded-md hover:border-text transition-all text-sm items-center w-full justify-between ${
34 | activeCategory === "" ? " text-accent" : ""
35 | }`}
36 | >
37 | {activeCategory === "" ? "All" : activeCategory}
38 |
39 |
40 |
41 |
42 | {isOpen && (
43 |
45 | handleCategoryChange("")}
47 | className={`py-4 px-4 flex gap-x-1 font-medium rounded border border-transparent hover:border-dim-gray transition-all text-sm ${
48 | activeCategory === "" ? "bg-accent text-bg" : " bg-bg text-accent"
49 | }`}
50 | >
51 | All
52 |
53 | {Object.entries(categoryCount).map(([item, count]) => {
54 | return (
55 | handleCategoryChange(item)}
58 | className={`py-4 px-4 flex gap-x-1 font-medium rounded border border-transparent hover:border-dim-gray transition-all text-sm relative ${
59 | activeCategory === item
60 | ? "text-bg bg-accent"
61 | : " bg-bg text-accent"
62 | }`}
63 | >
64 | {item}
65 |
70 | {count}
71 |
72 |
73 | );
74 | })}
75 |
76 | )}
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | fontSize: {
10 | 'display': ['clamp(2.75rem, 4.25vw, 6rem)', {
11 | lineHeight: '96.5%',
12 | letterSpacing: '-3.5%',
13 | fontWeight: '700'
14 | }],
15 | 'h1': '3.5rem',
16 | 'h2': '3rem',
17 | 'h3': '2.5rem',
18 | 'h4': '2rem',
19 | 'h5': '1.5rem',
20 | 'h6': '1.25rem',
21 | 'base': '1.125rem',
22 | 'sm': '1rem',
23 | 'xs': '0.875rem',
24 | 'xxs': '0.75rem',
25 | },
26 | colors: {
27 | 'transparent': 'transparent',
28 | 'bg': '#0C0D0E',
29 | 'dark-charcoal': '#101214',
30 | 'super-dark-gray': '#1A1A1A',
31 | 'dim-gray': '#555555',
32 | 'light-gray': '#878787',
33 | 'gray': '#D1D1D1',
34 | 'accent': '#F7F7F7',
35 | 'text': '#CCCCCC',
36 | 'primary': '#EFECE6',
37 | 'outline': '#D1D1D1',
38 | 'gradient': '#D0C7B3',
39 | 'gradient2': '#968A73',
40 | 'gradient3': '#675E4C',
41 | },
42 | extend: {
43 | fontFamily: {
44 | overusedgrotesk: ['var(--font-overusedgrotesk)'],
45 | },
46 |
47 | letterSpacing: {
48 | 'close': '-3.5%',
49 | 'base': '-1%'
50 | },
51 | lineHeight: {
52 | 'base': '150%'
53 | },
54 | boxShadow: {
55 | 'shine': '0px 0px 58px 20px rgba(85, 85, 85, 0.20);',
56 | 'bright': '0px 0px 58px 20px rgba(85, 85, 85, 0.35);'
57 | },
58 | borderOpacity: {
59 | '15': '0.15'
60 | },
61 | transitionTimingFunction: {
62 | 'in-out-circ': 'cubic-bezier(0.85, 0, 0.15, 1)',
63 | }
64 | },
65 | },
66 | plugins: [
67 | 'prettier-plugin-tailwindcss',
68 | require('@tailwindcss/container-queries'),
69 | ],
70 | }
71 |
--------------------------------------------------------------------------------