diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..e69de29 diff --git a/.env.example b/.env.example deleted file mode 100644 index 5359c0a..0000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_SITE_URL=site.url \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..e69de29 diff --git a/.eslintrc.json b/.eslintrc.json index 4f757ae..0655483 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +1,3 @@ { - "extends": [ - "next/core-web-vitals", - "standard", - "plugin:tailwindcss/recommended", - "prettier" - ] + "extends": ["next/core-web-vitals", "standard", "plugin:tailwindcss/recommended", "prettier"] } diff --git a/.github/ISSUE_TEMPLATE/another.md b/.github/ISSUE_TEMPLATE/another.md new file mode 100644 index 0000000..8b1f50c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/another.md @@ -0,0 +1,19 @@ +--- +name: ➕ Other Request +about: Use this template for any other type of request or remark +title: "[Other] " +labels: other +assignees: abdelkabirouadoukou +--- + +## Description + +Describe your request or remark. + +## Context + +Add any relevant context or examples. + +## Additional Information + +Add links, screenshots, or details if needed. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6297940 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: 🐞 Bug Report +about: Report a bug or issue +title: "[Bug] " +labels: bug +assignees: abdelkabirouadoukou +--- + +## Description + +Describe the bug you encountered. + +## Steps to Reproduce + +List the steps to reproduce the bug: + +1. ... +2. ... +3. ... + +## Expected Behavior + +Describe what you expected to happen. + +## Screenshots / Logs + +Add screenshots or logs if possible. + +## Additional Information + +- Project version: +- Operating system: +- Browser: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..d1ad858 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: 🚀 Feature Request +about: Propose a new feature for the project +title: "[Feature] " +labels: enhancement, Feature Request +assignees: abdelkabirouadoukou +--- + +## Description + +Describe the requested feature and its purpose. + +## Motivation + +Why is this feature important? + +## Proposed Solution + +Explain how it could be implemented. + +## Additional Information + +Add any relevant context or examples. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..febb5dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,19 @@ +--- +name: ❓ Question +about: Ask a question about the project +title: "[Question] " +labels: question +assignees: abdelkabirouadoukou +--- + +## Question + +Describe your question or request for help. + +## Context + +Add any relevant context or examples. + +## Additional Information + +Add links or details if needed. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..427c5e3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +# Pull Request + +## Description + +Briefly describe the changes you made and the motivation behind them. + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Enhancement +- [ ] Documentation update + +## Checklist + +- [ ] All tests pass +- [ ] Documentation is updated +- [ ] No conflicts with the main branch + +## Additional Information + +Add any relevant context, screenshots, or notes for reviewers. diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000..2e7573c --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,22 @@ +name: Auto Assign + +on: + issues: + types: [opened] + pull_request: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: 'Auto-assign issue' + uses: pozil/auto-assign-issue@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + assignees: abdelkabirouadoukou + numOfAssignee: 2 + reviewers: abdelkabirouadoukou \ No newline at end of file diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml new file mode 100644 index 0000000..96aa315 --- /dev/null +++ b/.github/workflows/code-check.yml @@ -0,0 +1,30 @@ +name: Code Format Check + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + format-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Bun + run: | + curl -fsSL https://bun.sh/install | bash + echo "$HOME/.bun/bin" >> $GITHUB_PATH + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install dependencies + run: bun ci + + - name: Check formatting + run: bun run format:check \ No newline at end of file diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..224740c --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,26 @@ +name: PR Checks + +on: + pull_request: + branches: [main] + +jobs: + lint-build-test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Bun + run: | + curl -fsSL https://bun.sh/install | bash + echo "$HOME/.bun/bin" >> $GITHUB_PATH + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + - name: Install dependencies + run: bun ci + - name: Lint + run: bun run lint + - name: Build + run: bun run build diff --git a/.gitignore b/.gitignore index 9928588..4eb83ba 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,10 @@ yarn-debug.log* yarn-error.log* # local env files -.env*.local -.env +.env.local +.env.development.local +.env.test.local +.env.production.local # vercel .vercel diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d1bb1c0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,24 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "useTabs": false, + "quoteProps": "as-needed", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "proseWrap": "preserve", + "requirePragma": false, + "vueIndentScriptAndStyle": false, + "experimentalTernaries": false, + "plugins": [ + "prettier-plugin-tailwindcss" + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4d17928 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:22-alpine + +WORKDIR /app +COPY package.json . +RUN bun i + +COPY . . +RUN bun run build + +CMD ["bun", "run", "start"] \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..92b9218 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,11 @@ +FROM node:22-alpine + +WORKDIR /app +COPY package.json . +RUN bun i + +COPY . . + +EXPOSE 3000 + +CMD ["bun", "run", "dev"] \ No newline at end of file diff --git a/bun.lock b/bun.lock index 37aa447..b288b73 100644 --- a/bun.lock +++ b/bun.lock @@ -7,10 +7,12 @@ "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "framer-motion": "^12.23.12", "core-js": "^3.45.1", "lucide-react": "^0.379.0", "next": "14.2.3", "next-sitemap": "^4.2.3", + "prettier-plugin-tailwindcss": "^0.6.14", "react": "^18", "react-dom": "^18", "rehype-autolink-headings": "^7.1.0", @@ -570,6 +572,8 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "framer-motion": ["framer-motion@12.23.12", "", { "dependencies": { "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -868,6 +872,10 @@ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "motion-dom": ["motion-dom@12.23.12", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw=="], + + "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -964,6 +972,8 @@ "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7f77941 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + env_file: + - .env.production.local + restart: unless-stopped + container_name: thinktapfast-blog-prod + + app-dev: + build: + context: . + dockerfile: Dockerfile.dev + ports: + - "3001:3000" + env_file: + - .env.development.local + volumes: + - .:/app + - /app/node_modules + - /app/.next + environment: + - NODE_ENV=development + - WATCHPACK_POLLING=true + restart: unless-stopped + container_name: thinktapfast-blog-dev \ No newline at end of file diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 48218ef..cbb6643 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -1,9 +1,9 @@ /** @type {import('next-sitemap').IConfig} */ module.exports = { - siteUrl: 'https://thinktapfast-blog.vercel.app', - generateRobotsTxt: true, // generate also robots.txt - changefreq: 'weekly', - priority: 0.7, - sitemapSize: 7000, - exclude: ['/admin/*', '/api/*'], // exclude URLs + siteUrl: "https://thinktapfast-blog.vercel.app", + generateRobotsTxt: true, // generate also robots.txt + changefreq: "weekly", + priority: 0.7, + sitemapSize: 7000, + exclude: ["/admin/*", "/api/*"], // exclude URLs }; diff --git a/package.json b/package.json index 53e72e8..8ba4cb4 100644 --- a/package.json +++ b/package.json @@ -5,19 +5,30 @@ "scripts": { "dev": "next dev", "build": "next build", + "build:sitemap": "next-sitemap", "start": "next start", "lint": "next lint", - "prettier": "pnpx prettier --write .", - "postbuild": "next-sitemap" + "lint:fix": "next lint --fix", + "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", + "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\"", + "docker:build": "docker build -t thinktapfast-app .", + "docker:run": "docker run -p 3000:3000 --env-file .env.production.local thinktapfast-app", + "docker:build:dev": "docker build -t thinktapfast-app-dev -f Dockerfile.dev .", + "docker:run:dev:cmd": "docker run -p 3000:3000 --env-file .env.development.local -v %cd%:/app -v /app/node_modules thinktapfast-app-dev", + "docker:run:dev:powershell": "powershell -ExecutionPolicy Bypass -File scripts/docker-dev.ps1", + "docker:run:dev:mac": "bash scripts/docker-dev.sh", + "docker:run:dev:linux": "bash scripts/docker-dev.sh" }, "dependencies": { "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "framer-motion": "^12.23.12", "core-js": "^3.45.1", "lucide-react": "^0.379.0", "next": "14.2.3", "next-sitemap": "^4.2.3", + "prettier-plugin-tailwindcss": "^0.6.14", "react": "^18", "react-dom": "^18", "rehype-autolink-headings": "^7.1.0", diff --git a/public/images/author/abdelkabir.jpeg b/public/images/author/abdelkabir.jpeg new file mode 100644 index 0000000..04db12a Binary files /dev/null and b/public/images/author/abdelkabir.jpeg differ diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml index ec1d401..39fed00 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,4 +1,3 @@ -https://thinktapfast-blog.vercel.app/sitemap-0.xml \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f8422..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/screenshot/mdx-blog-template.jpg b/screenshot/mdx-blog-template.jpg deleted file mode 100644 index e06a28f..0000000 Binary files a/screenshot/mdx-blog-template.jpg and /dev/null differ diff --git a/scripts/docker-dev.ps1 b/scripts/docker-dev.ps1 new file mode 100644 index 0000000..a5933e0 --- /dev/null +++ b/scripts/docker-dev.ps1 @@ -0,0 +1,4 @@ +# Docker development script for PowerShell +Write-Host "Starting Docker container for development..." -ForegroundColor Green +Write-Host "Current directory: $PWD" -ForegroundColor Yellow +docker run -p 3000:3000 --env-file .env.development.local -v "${PWD}:/app" -v /app/node_modules thinktapfast-blog-dev \ No newline at end of file diff --git a/scripts/docker-dev.sh b/scripts/docker-dev.sh new file mode 100644 index 0000000..e52d09d --- /dev/null +++ b/scripts/docker-dev.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Docker development script for Linux/WSL/macOS +echo "Starting Docker container for development..." +echo "Current directory: $(pwd)" +docker run -p 3000:3000 --env-file .env.development.local -v "$(pwd):/app" -v /app/node_modules thinktapfast-blog-dev \ No newline at end of file diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 6331b31..3b45470 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -3,62 +3,62 @@ import PageHeader from "@/components/page-header"; import Link from "next/link"; import Image from "next/image"; import { buttonVariants } from "@/components/ui/button"; -import { siteConfig } from "@/config/site"; +import { appConfig } from "@/config/app.config"; import { SOCIALS } from "@/constants"; import { cn } from "@/lib/utils"; import { Metadata } from "next"; -export const metadata:Metadata = { - title: 'About ThinkTapFast - AI Content Creation SaaS', - description: 'Learn about ThinkTapFast, an AI-powered SaaS that helps businesses, teams, and creators generate text, image, and voice content efficiently.', - keywords: 'ThinkTapFast, AI content creation, about, company, productivity, SaaS, automation', +export const metadata: Metadata = { + title: "About ThinkTapFast - AI Content Creation SaaS", + description: + "Learn about ThinkTapFast, an AI-powered SaaS that helps businesses, teams, and creators generate text, image, and voice content efficiently.", + keywords: "ThinkTapFast, AI content creation, about, company, productivity, SaaS, automation", openGraph: { - title: 'About ThinkTapFast - AI Content Creation SaaS', - description: 'Discover our mission, team, and story behind ThinkTapFast, the AI SaaS that saves time and costs for businesses and creators.', + title: "About ThinkTapFast - AI Content Creation SaaS", + description: + "Discover our mission, team, and story behind ThinkTapFast, the AI SaaS that saves time and costs for businesses and creators.", url: process.env.NEXT_PUBLIC_SITE_URL, - siteName: 'ThinkTapFast', + siteName: "ThinkTapFast", images: [ { - url: '/og-about.png', // About page-OG image + url: "/og-about.png", // About page-OG image width: 1200, height: 630, - alt: 'About ThinkTapFast', + alt: "About ThinkTapFast", }, ], - locale: 'en_US', - type: 'website', + locale: "en_US", + type: "website", }, twitter: { - card: 'summary_large_image', - title: 'About ThinkTapFast - AI Content Creation SaaS', - description: 'Meet the team and learn the mission behind ThinkTapFast, the AI-powered content creation SaaS.', - images: ['/og-about.png'], + card: "summary_large_image", + title: "About ThinkTapFast - AI Content Creation SaaS", + description: + "Meet the team and learn the mission behind ThinkTapFast, the AI-powered content creation SaaS.", + images: ["/og-about.png"], }, }; - export default function AboutPage() { return (

-
+
{siteConfig.name} -

{siteConfig.author}

-

- Web Developer -

+

{appConfig.author}

+

Web Developer

- {SOCIALS.map((social) => ( + {SOCIALS.map(social => ( @@ -77,15 +77,13 @@ export default function AboutPage() {

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Sequi, harum - odio! Molestias natus possimus dolorem modi libero eaque in aliquam - harum recusandae nam! Reprehenderit soluta fuga consequuntur, iure - corrupti autem! Lorem ipsum dolor sit amet consectetur adipisicing - elit. Modi asperiores voluptate, veritatis non placeat numquam. - Repellendus mollitia aut reprehenderit est. Reprehenderit soluta fuga - consequuntur, iure corrupti autem! Lorem ipsum dolor sit amet - consectetur adipisicing elit. Modi asperiores voluptate, veritatis non - placeat numquam. Repellendus mollitia aut reprehenderit est. + Lorem ipsum dolor sit amet consectetur adipisicing elit. Sequi, harum odio! Molestias + natus possimus dolorem modi libero eaque in aliquam harum recusandae nam! Reprehenderit + soluta fuga consequuntur, iure corrupti autem! Lorem ipsum dolor sit amet consectetur + adipisicing elit. Modi asperiores voluptate, veritatis non placeat numquam. Repellendus + mollitia aut reprehenderit est. Reprehenderit soluta fuga consequuntur, iure corrupti + autem! Lorem ipsum dolor sit amet consectetur adipisicing elit. Modi asperiores voluptate, + veritatis non placeat numquam. Repellendus mollitia aut reprehenderit est.

diff --git a/src/app/blog/[...slug]/page.tsx b/src/app/blog/[...slug]/page.tsx index 73f68cf..be23cad 100644 --- a/src/app/blog/[...slug]/page.tsx +++ b/src/app/blog/[...slug]/page.tsx @@ -2,15 +2,20 @@ import React from "react"; import type { Blog } from "@/types/globals"; import { Metadata } from "next"; import { blogs as allBlogs } from "#site/content"; -import { cn, formatDate } from "@/lib/utils"; +import { cn } from "@/lib/utils"; import "@/styles/mdx.css"; import Image from "next/image"; -import { siteConfig } from "@/config/site"; import { Mdx } from "@/components/mdx-component"; import { ChevronLeft } from "lucide-react"; import Link from "next/link"; import { buttonVariants } from "@/components/ui/button"; +import { BlogAuthorHeader } from "@/components/blog-author-header"; +import { BlogSidebar } from "@/components/blog-sidebar"; +import { ClientOnly } from "@/components/client-only"; +import { RelatedPosts } from "@/components/related-posts"; +import { BlogTags } from "@/components/blog-tags"; +import { TracingBeam } from "@/components/ui/tracing-beam"; interface BlogPageItemProps { readonly params: { @@ -27,9 +32,7 @@ async function getBlogFromParams(params: BlogPageItemProps["params"]) { return blog; } -export async function generateMetadata({ - params, -}: BlogPageItemProps): Promise { +export async function generateMetadata({ params }: BlogPageItemProps): Promise { const blog = await getBlogFromParams(params); if (!blog) { @@ -45,9 +48,7 @@ export async function generateMetadata({ }; } -export async function generateStaticParams(): Promise< - BlogPageItemProps["params"][] -> { +export async function generateStaticParams(): Promise { return allBlogs.map((blog: Blog) => ({ slug: blog.slugAsParams.split("/"), })); @@ -60,7 +61,7 @@ export default async function BlogPageItem({ params }: Readonly -

404 - Blog Not Found

+

404 - Blog Not Found

Sorry, the blog post you are looking for does not exist.

@@ -71,61 +72,72 @@ export default async function BlogPageItem({ params }: Readonly -
- {blog.date && ( - - )} - -

- {blog.title} -

- - {blog.author && ( -
- {blog.author} +
+ {/* Main Content Grid */} +
+ {/* Main Article Content */} +
+ {/* Enhanced Author Header Section */} + -
-

{blog.author}

-

- @{blog.author} -

+ +

+ {blog.title} +

+ + {/* Tags */} + {blog.tags && ( +
+ +
+ )} + + {blog.image && ( + {blog.title} + )} + + +
+ +
+
+ + {/* Related Posts */} + + +
+
+ + + See all Blogs +
-
- )} - - {blog.image && ( - {blog.title} - )} - -
-
- + + {/* Sidebar - Hidden on mobile, visible on desktop */} + } > - - See all Blogs - + +
- +
); -} \ No newline at end of file +} diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index 879558a..75eb92e 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -1,67 +1,114 @@ +"use client"; + import React from "react"; import type { Blog } from "@/types/globals"; -import { Metadata } from "next"; import PageHeader from "@/components/page-header"; import { blogs as allBlogs } from "#site/content"; import Image from "next/image"; import Link from "next/link"; import { formatDate } from "@/lib/utils"; - -export const metadata: Metadata = { - title: "Blog", -}; +import { getAuthor } from "@/config/authors"; +import { Clock } from "lucide-react"; +import { BlogTags } from "@/components/blog-tags"; export default function BlogPage() { - const blogs = allBlogs + const publishedBlogs = allBlogs .filter((blog: Blog) => blog.published) .sort((a: Blog, b: Blog) => new Date(b.date).getTime() - new Date(a.date).getTime()); + return ( -
+
+
- {blogs.length ? ( -
- {blogs.map((blog: Blog) => ( -
- {blog.image && ( - {blog.title} - )} + {/* Articles Grid */} +
+
+

All Articles

+

{publishedBlogs.length} articles published

+
-

- {blog.title} -

- {blog.description && ( -

{blog.description}

- )} + {publishedBlogs.length ? ( +
+ {publishedBlogs.map((blog: Blog) => { + const author = getAuthor(blog.author || ""); - {blog.date && ( -

- {formatDate(blog.date)} -

- )} + return ( +
+ {blog.image && ( +
+ {blog.title} + {blog.featured && ( +
+ Featured +
+ )} +
+ )} - - View Article - -
- ))} -
- ) : ( -

No Blogs found

- )} +
+
+

+ {blog.title} +

+ + {blog.description && ( +

+ {blog.description} +

+ )} + + {blog.tags && } + +
+
+ {blog.author} +
+

{blog.author}

+

{formatDate(blog.date)}

+
+
+ +
+ + {blog.readTime || 5}m +
+
+
+
+ + + Read {blog.title} + +
+ ); + })} +
+ ) : ( +
+

No articles found.

+
+ )} +
); -} \ No newline at end of file +} diff --git a/src/app/blog/tags/[tag]/page.tsx b/src/app/blog/tags/[tag]/page.tsx new file mode 100644 index 0000000..f21d8d0 --- /dev/null +++ b/src/app/blog/tags/[tag]/page.tsx @@ -0,0 +1,111 @@ +import React from "react"; +import type { Blog } from "@/types/globals"; +import { blogs as allBlogs } from "#site/content"; +import PageHeader from "@/components/page-header"; +import Link from "next/link"; +import { Tag } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface TagPageProps { + params: { + tag: string; + }; +} + +export default function TagPage({ params }: TagPageProps) { + const decodedTag = decodeURIComponent(params.tag); + + const taggedBlogs = allBlogs + .filter( + (blog: Blog) => + blog.published && blog.tags?.some(tag => tag.toLowerCase() === decodedTag.toLowerCase()), + ) + .sort((a: Blog, b: Blog) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + // Get all tags for tag cloud + const allTags = Array.from( + new Set( + allBlogs + .filter((blog: Blog) => blog.published && blog.tags) + .flatMap((blog: Blog) => blog.tags || []), + ), + ).sort(); + + return ( +
+ + +
+ + {/* Tag Cloud */} +
+

All Tags

+
+ {allTags.map(tag => { + const isActive = tag.toLowerCase() === decodedTag.toLowerCase(); + return ( + + + {tag} + + ); + })} +
+
+ + {/* Tagged Articles */} + {taggedBlogs.length ? ( +
+ {taggedBlogs.map((blog: Blog) => ( +
+
+

+ {blog.title} +

+ + {blog.description && ( +

+ {blog.description} +

+ )} + +
+ {blog.author} + {new Date(blog.date).toLocaleDateString()} +
+
+ + + View Article + +
+ ))} +
+ ) : ( +
+

+ No articles found with the tag "{decodedTag}". +

+ + ← Back to all articles + +
+ )} +
+ ); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000..00d1e69 Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/icon.svg b/src/app/icon.svg deleted file mode 100644 index 23c440d..0000000 --- a/src/app/icon.svg +++ /dev/null @@ -1 +0,0 @@ -MDX \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2f6aeaf..9c0465f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,6 +11,7 @@ import App from "@/components/app"; const lexend = Lexend({ subsets: ["latin"], variable: "--font-lexend" }); export const metadata: Metadata = { + metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000"), title: { template: "%s | Mdx Blog Template", default: "Mdx Blog Template", @@ -31,7 +32,7 @@ export default function RootLayout({
- {SOCIALS.map((social) => ( + {SOCIALS.map(social => ( @@ -56,22 +55,19 @@ export default function Home() { ))}

- A personal Blog template using{" "} - Mdx and{" "} - NextJs14 + Insights & Stories on AI,{" "} + Content Creation & Growth

- {siteConfig.description} + Explore how ThinkTapFast helps startups, creators, and businesses scale smarter with + AI-powered text, image, and voice content.

- 🎉My Blog + 📚 Read the Blog
diff --git a/src/components/app.tsx b/src/components/app.tsx index daa715d..adc78fc 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,24 +1,14 @@ import React, { PropsWithChildren } from "react"; import SiteHeader from "@/components/site-header"; -import { siteConfig } from "@/config/site"; -import Link from "next/link"; export default function App({ children }: PropsWithChildren) { return (
{children}
-
+

- © 2024 Created by{" "} - - {siteConfig.author} - {" "} + © 2025 ThinkTapFast. All rights reserved.

diff --git a/src/components/blog-author-header.tsx b/src/components/blog-author-header.tsx new file mode 100644 index 0000000..4b8394a --- /dev/null +++ b/src/components/blog-author-header.tsx @@ -0,0 +1,100 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { formatDate } from "@/lib/utils"; +import { getAuthor } from "@/config/authors"; +import { Github, Twitter } from "lucide-react"; + +interface BlogAuthorHeaderProps { + authorName: string; + publishDate?: string; + readTime?: string; +} + +export function BlogAuthorHeader({ authorName, publishDate, readTime }: BlogAuthorHeaderProps) { + const author = getAuthor(authorName); + + // Fallback if author not found in config + const authorData = author || { + name: authorName, + username: authorName.toLowerCase().replace(/\s+/g, ""), + avatar: "/images/author/abdelkabir.jpeg", + bio: "Blog author", + social: { + github: undefined, + twitter: undefined, + }, + }; + + return ( +
+ {/* Publication Info */} + {publishDate && ( +
+ + {readTime && ( + <> + + {readTime} read + + )} +
+ )} + + {/* Author Section */} +
+ {/* Author Avatar */} +
+ {authorData.name} +
+ + {/* Author Details */} +
+
+

{authorData.name}

+ @{authorData.username} +
+ + {authorData.bio && ( +

{authorData.bio}

+ )} + + {/* Social Links */} + {(authorData.social?.github || authorData.social?.twitter) && ( +
+ {authorData.social?.github && ( + + + GitHub + + )} + {authorData.social?.twitter && ( + + + Twitter + + )} +
+ )} +
+
+
+ ); +} diff --git a/src/components/blog-search.tsx b/src/components/blog-search.tsx new file mode 100644 index 0000000..6d5d592 --- /dev/null +++ b/src/components/blog-search.tsx @@ -0,0 +1,181 @@ +"use client"; + +import React, { useState, useMemo } from "react"; +import { Search, Filter, X, Calendar, User } from "lucide-react"; +import { Button } from "./ui/button"; +import { cn } from "@/lib/utils"; +import type { Blog } from "@/types/globals"; + +interface BlogSearchProps { + blogs: Blog[]; + onFilter: (filteredBlogs: Blog[]) => void; + className?: string; +} + +export function BlogSearch({ blogs, onFilter, className }: BlogSearchProps) { + const [searchTerm, setSearchTerm] = useState(""); + const [selectedAuthor, setSelectedAuthor] = useState(""); + const [selectedDateRange, setSelectedDateRange] = useState(""); + const [showFilters, setShowFilters] = useState(false); + + // Extract unique authors and dates + const authors = useMemo(() => { + const authorSet = new Set(blogs.map((blog: Blog) => blog.author).filter(Boolean)); + return Array.from(authorSet); + }, [blogs]); + + const dateRanges = useMemo(() => { + const currentYear = new Date().getFullYear(); + return [`${currentYear}`, `${currentYear - 1}`, "Older"]; + }, []); + + // Filter blogs based on search and filters + const filteredBlogs = useMemo(() => { + let filtered = blogs; + + // Search filter + if (searchTerm) { + filtered = filtered.filter( + (blog: Blog) => + blog.title.toLowerCase().includes(searchTerm.toLowerCase()) || + blog.description?.toLowerCase().includes(searchTerm.toLowerCase()) || + blog.author?.toLowerCase().includes(searchTerm.toLowerCase()) || + blog.tags?.some((tag: string) => tag.toLowerCase().includes(searchTerm.toLowerCase())), + ); + } + + // Author filter + if (selectedAuthor) { + filtered = filtered.filter((blog: Blog) => blog.author === selectedAuthor); + } + + // Date range filter + if (selectedDateRange) { + const currentYear = new Date().getFullYear(); + filtered = filtered.filter((blog: Blog) => { + const blogYear = new Date(blog.date).getFullYear(); + if (selectedDateRange === `${currentYear}`) { + return blogYear === currentYear; + } else if (selectedDateRange === `${currentYear - 1}`) { + return blogYear === currentYear - 1; + } else if (selectedDateRange === "Older") { + return blogYear < currentYear - 1; + } + return true; + }); + } + + return filtered; + }, [blogs, searchTerm, selectedAuthor, selectedDateRange]); + + // Update parent component with filtered results + React.useEffect(() => { + onFilter(filteredBlogs); + }, [filteredBlogs, onFilter]); + + const clearFilters = () => { + setSearchTerm(""); + setSelectedAuthor(""); + setSelectedDateRange(""); + }; + + const hasActiveFilters = searchTerm || selectedAuthor || selectedDateRange; + + return ( +
+ {/* Search Bar */} +
+ + setSearchTerm(e.target.value)} + className="w-full rounded-lg border bg-background py-2 pl-10 pr-4 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" + /> +
+ + {/* Filter Toggle */} +
+ + + {hasActiveFilters && ( + + )} +
+ + {/* Advanced Filters */} + {showFilters && ( +
+ {/* Author Filter */} +
+ + +
+ + {/* Date Range Filter */} +
+ + +
+
+ )} + + {/* Results Summary */} +
+ {filteredBlogs.length === blogs.length + ? `Showing all ${blogs.length} articles` + : `Showing ${filteredBlogs.length} of ${blogs.length} articles`} +
+
+ ); +} diff --git a/src/components/blog-sidebar.tsx b/src/components/blog-sidebar.tsx new file mode 100644 index 0000000..dd91030 --- /dev/null +++ b/src/components/blog-sidebar.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { TableOfContents } from "./table-of-contents"; + +interface BlogSidebarProps { + className?: string; + slug?: string; + publishDate?: string; + readTime?: number; +} + +export function BlogSidebar({ className, slug, publishDate, readTime }: BlogSidebarProps) { + return ( + + ); +} diff --git a/src/components/blog-tags.tsx b/src/components/blog-tags.tsx new file mode 100644 index 0000000..5d323d8 --- /dev/null +++ b/src/components/blog-tags.tsx @@ -0,0 +1,45 @@ +import { Tag } from "lucide-react"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +interface BlogTagsProps { + tags: string[]; + className?: string; + variant?: "default" | "outline" | "secondary"; + size?: "sm" | "md" | "lg"; +} + +export function BlogTags({ tags, className, variant = "default", size = "md" }: BlogTagsProps) { + if (!tags || tags.length === 0) return null; + + const sizeClasses = { + sm: "text-xs px-2 py-1", + md: "text-sm px-3 py-1", + lg: "text-base px-4 py-2", + }; + + const variantClasses = { + default: "bg-primary/10 text-primary hover:bg-primary/20", + outline: "border border-primary/20 text-primary hover:border-primary/40", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + }; + + return ( +
+ + {tags.map(tag => ( + + {tag} + + ))} +
+ ); +} diff --git a/src/components/client-only.tsx b/src/components/client-only.tsx new file mode 100644 index 0000000..c50ce27 --- /dev/null +++ b/src/components/client-only.tsx @@ -0,0 +1,22 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +interface ClientOnlyProps { + children: React.ReactNode; + fallback?: React.ReactNode; +} + +export function ClientOnly({ children, fallback = null }: ClientOnlyProps) { + const [hasMounted, setHasMounted] = useState(false); + + useEffect(() => { + setHasMounted(true); + }, []); + + if (!hasMounted) { + return <>{fallback}; + } + + return <>{children}; +} diff --git a/src/components/header-nav.tsx b/src/components/header-nav.tsx index 76e7361..29a6e62 100644 --- a/src/components/header-nav.tsx +++ b/src/components/header-nav.tsx @@ -9,15 +9,13 @@ export default function HeaderNav() { const segment = useSelectedLayoutSegment(); return (
), tr: ({ className, ...props }: React.HTMLAttributes) => ( - + ), th: ({ className, ...props }: ComponentsProps) => ( ), - pre: ({ className, ...props }: ComponentsProps) => ( -
-  ),
+  pre: ({ className, children, ...props }: ComponentsProps) => {
+    // Extract code content and language from children
+    const codeElement = React.Children.toArray(children).find(
+      (child): child is React.ReactElement => React.isValidElement(child) && child.type === "code",
+    );
+
+    if (codeElement && codeElement.props) {
+      const codeContent = codeElement.props.children;
+      const language = codeElement.props.className?.replace("language-", "") || "text";
+
+      return ;
+    }
+
+    // Fallback to regular pre if no code element found
+    return (
+      
+        {children}
+      
+ ); + }, code: ({ className, ...props }: ComponentsProps) => ( -
- {NAV_LIST.map((item) => ( +
+ {NAV_LIST.map(item => ( { +const MobileLink = ({ children, onOpenChange, className, href, ...props }: MobileLinkProps) => { const router = useRouter(); const pathname = usePathname(); return ( diff --git a/src/components/page-header.tsx b/src/components/page-header.tsx index ef7688d..5d8cfed 100644 --- a/src/components/page-header.tsx +++ b/src/components/page-header.tsx @@ -12,9 +12,7 @@ export default function PageHeader({ title, description }: PageHeaderProps) {

{title}

- {description && ( -

{description}

- )} + {description &&

{description}

}
); diff --git a/src/components/related-posts.tsx b/src/components/related-posts.tsx new file mode 100644 index 0000000..a3bbf46 --- /dev/null +++ b/src/components/related-posts.tsx @@ -0,0 +1,116 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { formatDate } from "@/lib/utils"; +import { Clock, ArrowRight } from "lucide-react"; +import { getAuthor } from "@/config/authors"; +import type { Blog } from "@/types/globals"; + +interface RelatedPostsProps { + currentPost: Blog; + allPosts: Blog[]; + maxPosts?: number; +} + +export function RelatedPosts({ currentPost, allPosts, maxPosts = 3 }: RelatedPostsProps) { + // Find related posts based on author and exclude current post + const relatedPosts = React.useMemo(() => { + const otherPosts = allPosts.filter(post => post.slug !== currentPost.slug && post.published); + + // Prioritize posts by same author + const sameAuthorPosts = otherPosts.filter(post => post.author === currentPost.author); + + // Get other recent posts if we don't have enough from same author + const recentPosts = otherPosts + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, maxPosts * 2); + + // Combine and deduplicate + const combined = [...sameAuthorPosts, ...recentPosts]; + const unique = combined.filter( + (post, index, arr) => arr.findIndex(p => p.slug === post.slug) === index, + ); + + return unique.slice(0, maxPosts); + }, [currentPost, allPosts, maxPosts]); + + if (relatedPosts.length === 0) return null; + + return ( +
+
+

Related Articles

+

Continue reading with these related posts

+
+ +
+ {relatedPosts.map(post => { + const author = getAuthor(post.author || ""); + + return ( +
+ {post.image && ( +
+ {post.title} +
+ )} + +
+

+ {post.title} +

+ + {post.description && ( +

+ {post.description} +

+ )} + +
+
+ {author?.avatar && ( + {post.author + )} + {post.author} +
+ +
+ + 5m read +
+
+ +
+ + + +
+
+ + + Read {post.title} + +
+ ); + })} +
+
+ ); +} diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index 2cfc0bd..8aefb95 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -2,11 +2,11 @@ import React, { useState } from "react"; import Link from "next/link"; import { AlignLeft, X } from "lucide-react"; -import { siteConfig } from "@/config/site"; +import { appConfig } from "@/config/app.config"; import HeaderNav from "@/components/header-nav"; import { Button } from "@/components/ui/button"; import MobileNav from "@/components/mobile-nav"; -import { Icons } from "./icons"; +// import { Icons } from "./icons"; export default function SiteHeader() { const [isMobileOpen, setIsMobileOpen] = useState(false); @@ -15,8 +15,8 @@ export default function SiteHeader() {
- - {siteConfig.name} + {/* */} + {appConfig.name}
@@ -27,19 +27,13 @@ export default function SiteHeader() { onClick={() => setIsMobileOpen(!isMobileOpen)} > <> - {isMobileOpen ? ( - - ) : ( - - )} + {isMobileOpen ? : } Menu
- {isMobileOpen && ( - setIsMobileOpen(false)} /> - )} + {isMobileOpen && setIsMobileOpen(false)} />} ); } diff --git a/src/components/table-of-contents.tsx b/src/components/table-of-contents.tsx new file mode 100644 index 0000000..37c0d03 --- /dev/null +++ b/src/components/table-of-contents.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; +import { List } from "lucide-react"; + +interface TocItem { + id: string; + title: string; + level: number; +} + +interface TableOfContentsProps { + className?: string; +} + +export function TableOfContents({ className }: TableOfContentsProps) { + const [toc, setToc] = useState([]); + const [activeId, setActiveId] = useState(""); + + useEffect(() => { + // Extract headings from the content (skip h1, focus on h2-h4) + const headings = document.querySelectorAll("h2, h3, h4"); + const tocItems: TocItem[] = []; + + headings.forEach((heading, index) => { + const id = heading.id || `heading-${index}`; + if (!heading.id) { + heading.id = id; + } + + tocItems.push({ + id, + title: heading.textContent || "", + level: parseInt(heading.tagName.charAt(1)), + }); + }); + + setToc(tocItems); + + // Set up intersection observer for active heading + const observer = new IntersectionObserver( + entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { + rootMargin: "-20% 0% -80% 0%", + threshold: 0, + }, + ); + + headings.forEach(heading => observer.observe(heading)); + + return () => observer.disconnect(); + }, []); + + const scrollToHeading = (id: string) => { + const element = document.getElementById(id); + if (element) { + element.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } + }; + + if (toc.length === 0) return null; + + return ( +
+
+
+ +

Contents

+
+ + +
+
+ ); +} diff --git a/src/components/ui/background-lines.tsx b/src/components/ui/background-lines.tsx new file mode 100644 index 0000000..ff80e46 --- /dev/null +++ b/src/components/ui/background-lines.tsx @@ -0,0 +1,71 @@ +"use client"; +import React from "react"; +import { cn } from "@/lib/utils"; + +export const BackgroundLines = ({ + children, + className, + svgOptions, +}: { + children: React.ReactNode; + className?: string; + svgOptions?: { + duration?: number; + }; +}) => { + return ( +
+ +
{children}
+
+ ); +}; + +const SVGGrid = ({ + svgOptions, +}: { + svgOptions?: { + duration?: number; + }; +}) => { + return ( + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index c552fae..c492390 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -10,12 +10,9 @@ const buttonVariants = cva( variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, @@ -43,11 +40,7 @@ const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ( - + ); }, ); diff --git a/src/components/ui/code-block.tsx b/src/components/ui/code-block.tsx new file mode 100644 index 0000000..f1d51f2 --- /dev/null +++ b/src/components/ui/code-block.tsx @@ -0,0 +1,65 @@ +"use client"; +import { useState } from "react"; +import { Check, Copy } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface CodeBlockProps { + code: string; + language?: string; + filename?: string; + className?: string; +} + +export function CodeBlock({ code, language = "javascript", filename, className }: CodeBlockProps) { + const [copied, setCopied] = useState(false); + + const copyToClipboard = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* Header */} +
+
+
+
+
+
+
+ {filename || `${language || "text"}`} +
+ +
+ + {/* Code Content */} +
+
+          
+            {code}
+          
+        
+ + {/* Gradient overlay for long code */} +
+
+
+ ); +} diff --git a/src/components/ui/tracing-beam.tsx b/src/components/ui/tracing-beam.tsx new file mode 100644 index 0000000..76b6f6a --- /dev/null +++ b/src/components/ui/tracing-beam.tsx @@ -0,0 +1,126 @@ +"use client"; +import React, { useEffect, useRef, useState } from "react"; +import { motion, useTransform, useScroll, useSpring } from "framer-motion"; +import { cn } from "@/lib/utils"; + +export const TracingBeam = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + const ref = useRef(null); + const { scrollYProgress } = useScroll({ + target: ref, + offset: ["start start", "end end"], + }); + + const contentRef = useRef(null); + const [svgHeight, setSvgHeight] = useState(800); + + useEffect(() => { + if (contentRef.current) { + setSvgHeight(contentRef.current.offsetHeight); + } + + const handleResize = () => { + if (contentRef.current) { + setSvgHeight(contentRef.current.offsetHeight); + } + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [children]); + + const y1 = useSpring(useTransform(scrollYProgress, [0, 0.7], [50, svgHeight - 100]), { + stiffness: 500, + damping: 90, + }); + const y2 = useSpring(useTransform(scrollYProgress, [0, 1], [50, svgHeight - 50]), { + stiffness: 500, + damping: 90, + }); + + return ( + +
+ + + + +
+
+ {children} +
+
+ ); +}; diff --git a/src/config/app.config.ts b/src/config/app.config.ts new file mode 100644 index 0000000..1b709b7 --- /dev/null +++ b/src/config/app.config.ts @@ -0,0 +1,13 @@ +import authorAvatar from "../../public/images/author/abdelkabir.jpeg"; +export const appConfig = { + name: "ThinkTapFast | Blog", + description: + "MDX Blog Template is a simple implementation of a markdown static blog. Built with Next.js 14 and velite js.", + author: "ThinkTapFast Team", + authorImage: authorAvatar, + social: { + github: "https://github.com/ThinkTapFast", + }, +}; + +export type AppConfig = typeof appConfig; diff --git a/src/config/authors.ts b/src/config/authors.ts new file mode 100644 index 0000000..1f0f8b1 --- /dev/null +++ b/src/config/authors.ts @@ -0,0 +1,41 @@ +// Author profiles for blog posts +export const authors = { + Abdelkabir: { + name: "Abdelkabir", + username: "abdelkabir", + avatar: "/images/author/abdelkabir.jpeg", + bio: "Full-stack developer passionate about modern web technologies", + social: { + github: "https://github.com/abdelkabir", + twitter: "https://twitter.com/abdelkabir", + }, + }, + devbertskie: { + name: "Dev Bertskie", + username: "devbertskie", + avatar: "/images/author/devbertskie.png", + bio: "Software engineer and tech enthusiast", + social: { + github: "https://github.com/devbertskie", + twitter: "https://twitter.com/devbertskie", + }, + }, + "ThinkTapFast Team": { + name: "ThinkTapFast Team", + username: "thinktapfast", + avatar: "/images/author/abdelkabir.jpeg", // Default team avatar + bio: "Tech team building innovative solutions", + social: { + github: "https://github.com/ThinkTapFast", + twitter: undefined, + }, + }, +} as const; + +export type AuthorKey = keyof typeof authors; +export type Author = (typeof authors)[AuthorKey]; + +// Helper function to get author by name +export function getAuthor(authorName: string): Author | null { + return authors[authorName as AuthorKey] || null; +} diff --git a/src/config/site.ts b/src/config/site.ts deleted file mode 100644 index b905af9..0000000 --- a/src/config/site.ts +++ /dev/null @@ -1,15 +0,0 @@ -import authorAvatar from "../../public/images/author/devbertskie.png"; -export const siteConfig = { - name: "Mdx Blog Template", - description: - "MDX Blog Template is a simple implementation of a markdown static blog. Built with Next.js 14 and velite js.", - author: "devbertskie", - authorImage: authorAvatar, - social: { - github: "https://github.com/devbertskie", - twitter: "https://twitter.com", - facebook: "https://facebook.com", - }, -}; - -export type SiteConfig = typeof siteConfig; \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index 86a88d2..350f28d 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,5 +1,5 @@ import { Icons } from "@/components/icons"; -import { siteConfig } from "@/config/site"; +import { appConfig } from "@/config/app.config"; import { Bot, Rss } from "lucide-react"; export const NAV_LIST = [ @@ -7,8 +7,4 @@ export const NAV_LIST = [ { label: "About", path: "/about", icon: Bot }, ]; -export const SOCIALS = [ - { label: "Github", path: siteConfig.social.github, icon: Icons.github }, - { label: "Facebook", path: siteConfig.social.facebook, icon: Icons.facebook }, - { label: "Twitter", path: siteConfig.social.twitter, icon: Icons.x }, -]; +export const SOCIALS = [{ label: "Github", path: appConfig.social.github, icon: Icons.github }]; diff --git a/src/content/blog/advanced-blog-features.mdx b/src/content/blog/advanced-blog-features.mdx new file mode 100644 index 0000000..affe66c --- /dev/null +++ b/src/content/blog/advanced-blog-features.mdx @@ -0,0 +1,144 @@ +--- +title: "[Example] Advanced Next.js Blog Features" +description: "Explore the enhanced blog layout with sidebar, table of contents, reading progress, and improved UI components" +image: "/images/blog/extends.webp" +date: "2025-09-04" +author: "Abdelkabir" +tags: ["nextjs", "react", "blog", "ui", "typescript"] +readTime: 12 +featured: true +--- + +# Advanced Next.js Blog Features + +Welcome to our enhanced blog experience! This post demonstrates all the new features we've implemented to make reading and navigating our content more enjoyable. + +## Enhanced User Interface + +Our blog now features a modern, responsive design with several key improvements: + +### Sidebar Navigation +The right sidebar provides quick access to essential features: +- **Table of Contents** - Navigate directly to any section +- **Reading Progress** - Track your progress through the article +- **Quick Actions** - Share, bookmark, and like articles +- **Article Information** - Reading time and publication details + +### Improved Blog Listing +The blog index page now showcases: +- **Card-based Layout** - Clean, modern post previews +- **Author Information** - Profile pictures and social links +- **Reading Time Estimates** - Know what to expect before diving in +- **Enhanced Typography** - Better readability and visual hierarchy + +## Technical Implementation + +### Table of Contents Generation +Our TOC system automatically: +1. **Scans Headings** - Extracts all H1-H6 elements from content +2. **Generates IDs** - Creates clean, URL-friendly anchor links +3. **Tracks Progress** - Highlights the current section while reading +4. **Smooth Scrolling** - Provides seamless navigation between sections + +### Reading Progress Tracking +The progress indicator: +- **Calculates Percentage** - Based on scroll position relative to content +- **Visual Feedback** - Shows completion status at a glance +- **Responsive Design** - Adapts to different screen sizes + +### Author System +We've implemented a comprehensive author management system: +- **Author Profiles** - Detailed information for each contributor +- **Social Links** - Connect with authors on GitHub and Twitter +- **Consistent Branding** - Unified presentation across all posts + +## Browser Compatibility + +Thanks to our core-js integration, all these features work seamlessly across: + +### Modern Browsers +- ✅ Chrome 90+ +- ✅ Firefox 88+ +- ✅ Safari 14+ +- ✅ Edge 90+ + +### Legacy Support +- ✅ Internet Explorer 11 +- ✅ Chrome 40+ +- ✅ Firefox 35+ +- ✅ Safari 9+ + +## Performance Optimizations + +### Image Handling +- **Next.js Image Component** - Automatic optimization and lazy loading +- **Responsive Images** - Serves appropriate sizes for different devices +- **Modern Formats** - WebP support with fallbacks + +### Code Splitting +- **Dynamic Imports** - Components load only when needed +- **Bundle Optimization** - Reduced initial page load times +- **Progressive Enhancement** - Core functionality works without JavaScript + +## Mobile Experience + +The enhanced design is fully responsive: + +### Touch-Friendly Interface +- **Large Touch Targets** - Easy navigation on mobile devices +- **Gesture Support** - Smooth scrolling and interactions +- **Optimized Typography** - Readable text at all screen sizes + +### Mobile-First Design +- **Progressive Disclosure** - Sidebar collapses on mobile +- **Optimized Navigation** - Easy access to all features +- **Fast Loading** - Optimized for mobile networks + +## Accessibility Features + +We've prioritized accessibility throughout: + +### Keyboard Navigation +- **Tab Order** - Logical navigation flow +- **Focus Indicators** - Clear visual feedback +- **Skip Links** - Quick access to main content + +### Screen Reader Support +- **Semantic HTML** - Proper heading hierarchy +- **ARIA Labels** - Descriptive text for interactive elements +- **Alt Text** - Comprehensive image descriptions + +## Content Management + +### MDX Integration +- **Rich Content** - Combine Markdown with React components +- **Syntax Highlighting** - Beautiful code blocks +- **Interactive Elements** - Embed dynamic content + +### Metadata Handling +- **SEO Optimization** - Proper meta tags and structured data +- **Social Sharing** - Open Graph and Twitter Card support +- **Automated Sitemap** - Keep search engines updated + +## Future Enhancements + +We're continuously improving the blog experience: + +### Planned Features +- **Search Functionality** - Find content quickly +- **Comment System** - Engage with readers +- **Related Posts** - Discover more content +- **Tags and Categories** - Better content organization + +### Performance Improvements +- **Service Worker** - Offline reading capability +- **Prefetching** - Faster navigation between posts +- **CDN Integration** - Global content delivery + +## Conclusion + +These enhancements represent a significant step forward in our blogging platform. The combination of modern design, accessibility features, and performance optimizations creates an exceptional reading experience. + +Whether you're browsing on a desktop with the full sidebar experience or reading on mobile with our responsive design, every feature is crafted to help you focus on what matters most: the content. + +We're excited to continue improving and adding new features based on your feedback. Happy reading! diff --git a/src/content/blog/core-js-features-demo.mdx b/src/content/blog/core-js-features-demo.mdx new file mode 100644 index 0000000..c5c126d --- /dev/null +++ b/src/content/blog/core-js-features-demo.mdx @@ -0,0 +1,98 @@ +--- +title: "[Example] Core-js Powers Modern JavaScript Features" +description: "See how core-js enables modern JavaScript in your blog content, even on older browsers" +image: "/images/blog/any-considered.webp" +date: "2025-09-04" +author: "Abdelkabir" +tags: ["javascript", "core-js", "polyfills", "browser-compatibility"] +readTime: 8 +--- + +# Core-js Features in Action + +This blog post demonstrates how **core-js polyfills** enable modern JavaScript features in your MDX content, ensuring compatibility across all browsers. + +## Array Methods (Polyfilled by Core-js) + +```javascript +// These methods work in IE11+ thanks to core-js +const posts = ['intro', 'advanced', 'tutorial']; + +// Array.includes (IE11 needs polyfill) +const hasIntro = posts.includes('intro'); // ✅ Works everywhere + +// Array.find (IE11 needs polyfill) +const foundPost = posts.find(post => post.startsWith('adv')); // ✅ Works everywhere + +// Array.findIndex (IE11 needs polyfill) +const introIndex = posts.findIndex(post => post === 'intro'); // ✅ Works everywhere +``` + +## Object Methods (Polyfilled by Core-js) + +```javascript +// Modern object methods work across all browsers +const blogMeta = { title: 'My Post', date: '2025-09-04' }; + +// Object.entries (IE11 needs polyfill) +Object.entries(blogMeta).forEach(([key, value]) => { + console.log(`${key}: ${value}`); // ✅ Works everywhere +}); + +// Object.values (IE11 needs polyfill) +const values = Object.values(blogMeta); // ✅ Works everywhere + +// Object.assign (IE11 needs polyfill) +const extendedMeta = Object.assign({}, blogMeta, { author: 'You' }); // ✅ Works everywhere +``` + +## String Methods (Polyfilled by Core-js) + +```javascript +const postTitle = "How to use Core-js in your MDX blog"; + +// String.includes (IE11 needs polyfill) +const isAboutCoreJs = postTitle.includes('Core-js'); // ✅ Works everywhere + +// String.startsWith (IE11 needs polyfill) +const isHowTo = postTitle.startsWith('How to'); // ✅ Works everywhere + +// String.endsWith (IE11 needs polyfill) +const isAboutBlog = postTitle.endsWith('blog'); // ✅ Works everywhere +``` + +## Modern Collections (Polyfilled by Core-js) + +```javascript +// Map and Set work in older browsers +const tagMap = new Map([ + ['javascript', 'JavaScript'], + ['typescript', 'TypeScript'], + ['react', 'React'] +]); // ✅ Works in IE11+ + +const uniqueTags = new Set(['javascript', 'typescript', 'javascript']); // ✅ Works in IE11+ +``` + +## Browser Compatibility + +Thanks to core-js, this blog supports: + +- ✅ **Internet Explorer 11+** +- ✅ **Chrome 40+** +- ✅ **Firefox 35+** +- ✅ **Safari 9+** +- ✅ **Edge (all versions)** +- ✅ **Mobile browsers** + +## Development Features + +In development mode, you can see polyfill activity in the browser console: + +```javascript +// Console output examples: +console.log('🔧 Core-js polyfills applied for: Array.includes, String.startsWith, Map'); +console.log('✅ Modern browser detected - all features supported natively'); +``` + +Your MDX content can use modern JavaScript features confidently, knowing core-js provides seamless backward compatibility! diff --git a/src/content/blog/intro.mdx b/src/content/blog/intro.mdx deleted file mode 100644 index 2414612..0000000 --- a/src/content/blog/intro.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Hello World" -description: "Discover when Blog" -image: "/images/blog/any-considered.webp" -date: "2025-08-03" -author: "abdelkabir" ---- -hello \ No newline at end of file diff --git a/src/content/blog/welcome.mdx b/src/content/blog/welcome.mdx new file mode 100644 index 0000000..6ee7cac --- /dev/null +++ b/src/content/blog/welcome.mdx @@ -0,0 +1,32 @@ +--- +title: "Welcome" +description: "Discover when Blog" +image: "/images/blog/any-considered.webp" +date: "2025-08-03" +author: "Abdelkabir" +tags: ["welcome", "introduction", "blog"] +readTime: 3 +featured: true +--- + +# Welcome to Our Blog! + +Hello and welcome to our tech blog! This is where we share insights, tutorials, and thoughts about modern web development. + +## What You'll Find Here + +- **Modern Web Development**: Latest trends in React, Next.js, and TypeScript +- **Best Practices**: Code quality, testing, and deployment strategies +- **Tutorials**: Step-by-step guides to build amazing applications +- **Personal Insights**: Our experiences and lessons learned + +## Getting Started + +This blog is built with: +- **Next.js 14** with App Router +- **TypeScript** for type safety +- **Tailwind CSS** for styling +- **MDX** for rich content +- **Core-js** for browser compatibility + +We hope you find our content helpful and engaging. Happy reading! \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index d85a87b..a4febbf 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2,6 +2,24 @@ @tailwind components; @tailwind utilities; +@layer utilities { + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + } + + .scrollbar-hide::-webkit-scrollbar { + display: none; + } +} + @layer base { :root { --background: 270 100% 95%; diff --git a/src/styles/mdx.css b/src/styles/mdx.css index 921faa6..0e3dc77 100644 --- a/src/styles/mdx.css +++ b/src/styles/mdx.css @@ -3,7 +3,7 @@ } [data-rehype-pretty-code-figure] code { - @apply text-sm !leading-loose md:text-base border-0 p-0; + @apply border-0 p-0 text-sm !leading-loose md:text-base; } [data-rehype-pretty-code-figure] code[data-line-numbers] { @@ -35,5 +35,5 @@ } .subheading-anchor { - @apply no-underline before:content-["#"] hover:before:content-["#"] -ml-4 before:text-background hover:before:text-primary transition-colors before:mr-1 before:text-lg; + @apply -ml-4 no-underline transition-colors before:mr-1 before:text-lg before:text-background before:content-["#"] hover:before:text-primary hover:before:content-["#"]; } diff --git a/src/types/globals.ts b/src/types/globals.ts index 49d1a7d..8a3e6a7 100644 --- a/src/types/globals.ts +++ b/src/types/globals.ts @@ -8,4 +8,7 @@ export interface Blog { image?: string; author: string; body: string; + tags?: string[]; + readTime?: number; + featured?: boolean; } diff --git a/tsconfig.json b/tsconfig.json index d367136..d23961f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,8 @@ "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", - "./.velite" + "./.velite", + "next-sitemap.config.js" ], "exclude": ["node_modules"] } diff --git a/velite.config.ts b/velite.config.ts index 824cb2a..18cadcb 100644 --- a/velite.config.ts +++ b/velite.config.ts @@ -20,6 +20,9 @@ const blogs = defineCollection({ published: s.boolean().default(true), image: s.string().max(99), author: s.string(), + tags: s.array(s.string()).optional(), + readTime: s.number().optional(), + featured: s.boolean().optional(), body: s.mdx(), }) .transform(computedFields),