├── .github └── workflows │ └── nextjs.yml ├── .gitignore ├── README.md ├── components.json ├── next.config.js ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── blog │ │ ├── NavTrigger.tsx │ │ ├── [slug] │ │ │ ├── TocTrigger.tsx │ │ │ └── page.tsx │ │ ├── _data │ │ │ ├── author.ts │ │ │ └── posts.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── chat │ │ ├── ChatItem.tsx │ │ ├── ChatList.tsx │ │ ├── _data │ │ │ ├── mock-chats.ts │ │ │ ├── mock-messages.ts │ │ │ └── mock-settings.ts │ │ └── page.tsx │ ├── dashboard │ │ ├── _data │ │ │ └── menu.ts │ │ ├── page.tsx │ │ └── triggers.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── workshop │ │ ├── blog │ │ ├── [slug] │ │ │ └── page.tsx │ │ ├── guide.md │ │ └── page.tsx │ │ ├── chat │ │ ├── guide.md │ │ └── page.tsx │ │ └── dashboard │ │ ├── guide.md │ │ └── page.tsx ├── components │ ├── blog │ │ ├── AuthorProfile.tsx │ │ ├── BlogFooter.tsx │ │ ├── Markdown.tsx │ │ ├── NewsletterForm.tsx │ │ ├── PostCard.tsx │ │ └── TableOfContents.tsx │ ├── chat │ │ ├── ChatItem.tsx │ │ ├── ChatList.tsx │ │ ├── ChatSetting.tsx │ │ ├── Conversation.tsx │ │ └── Input.tsx │ ├── dashboard │ │ ├── AppSwitcher.tsx │ │ ├── CollapsibleMenu.tsx │ │ ├── OrderStats.tsx │ │ ├── OrderTable.tsx │ │ ├── UserSetting.tsx │ │ └── _data │ │ │ ├── mock-orders.ts │ │ │ └── mock-stats.ts │ └── ui │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── screen-size-indicator.tsx │ │ ├── scroll-area.tsx │ │ ├── separator.tsx │ │ ├── table.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx └── lib │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json /.github/workflows/nextjs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages 2 | # 3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started 4 | # 5 | name: Deploy Next.js site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ["main"] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Detect package manager 35 | id: detect-package-manager 36 | run: | 37 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 38 | echo "manager=yarn" >> $GITHUB_OUTPUT 39 | echo "command=install" >> $GITHUB_OUTPUT 40 | echo "runner=yarn" >> $GITHUB_OUTPUT 41 | exit 0 42 | elif [ -f "${{ github.workspace }}/package.json" ]; then 43 | echo "manager=npm" >> $GITHUB_OUTPUT 44 | echo "command=ci" >> $GITHUB_OUTPUT 45 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 46 | exit 0 47 | else 48 | echo "Unable to determine package manager" 49 | exit 1 50 | fi 51 | - name: Setup Node 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: "20" 55 | cache: ${{ steps.detect-package-manager.outputs.manager }} 56 | - name: Setup Pages 57 | uses: actions/configure-pages@v5 58 | with: 59 | # Automatically inject basePath in your Next.js configuration file and disable 60 | # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). 61 | # 62 | # You may remove this line if you want to manage the configuration yourself. 63 | static_site_generator: next 64 | - name: Restore cache 65 | uses: actions/cache@v4 66 | with: 67 | path: | 68 | .next/cache 69 | # Generate a new cache whenever packages or source files change. 70 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 71 | # If source files changed but packages didn't, rebuild from a prior cache. 72 | restore-keys: | 73 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- 74 | - name: Install dependencies 75 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 76 | - name: Build with Next.js 77 | run: ${{ steps.detect-package-manager.outputs.runner }} next build 78 | - name: Upload artifact 79 | uses: actions/upload-pages-artifact@v3 80 | with: 81 | path: ./out 82 | 83 | # Deployment job 84 | deploy: 85 | environment: 86 | name: github-pages 87 | url: ${{ steps.deployment.outputs.page_url }} 88 | runs-on: ubuntu-latest 89 | needs: build 90 | steps: 91 | - name: Deploy to GitHub Pages 92 | id: deployment 93 | uses: actions/deploy-pages@v4 94 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a repo for learning how to build layout with [Tailwind Jun Layout](https://tailwindcss-jun-layout.vercel.app/). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies and run the development server: 6 | 7 | ```bash 8 | npm install && npm run dev 9 | ``` 10 | 11 | Open [http://localhost:3333](http://localhost:3333) with your browser to see the result. 12 | 13 | There are 3 layout practices in this repo: 14 | 15 | - Dashboard 16 | - Chat 17 | - Blog 18 | 19 | ## Dashboard layout 20 | 21 | Build a dashboard layout at `src/app/workshop/dashboard/page.tsx`. 22 | 23 | Visit a completed version url `/app/dashboard` as a reference. 24 | 25 | ## Chat 26 | 27 | Build a chat app layout at `src/app/workshop/chat/page.tsx`. 28 | 29 | Visit a completed version url `/app/chat` as a reference. 30 | 31 | ## Blog 32 | 33 | Build a chat app layout at `src/app/workshop/blog/page.tsx`. 34 | 35 | Visit a completed version url `/app/blog` as a reference. 36 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: "export", 4 | images: { 5 | unoptimized: true, 6 | }, 7 | }; 8 | 9 | module.exports = nextConfig; 10 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | output: "export", 5 | assetPrefix: 6 | process.env.NODE_ENV === "production" 7 | ? "/learn-tailwindcss-jun-layout" 8 | : "", 9 | basePath: 10 | process.env.NODE_ENV === "production" 11 | ? "/learn-tailwindcss-jun-layout" 12 | : "", 13 | }; 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-tailwindcss-jun-layout", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3333", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-avatar": "1.1.1", 13 | "@radix-ui/react-dropdown-menu": "2.1.2", 14 | "@radix-ui/react-scroll-area": "1.2.1", 15 | "@radix-ui/react-separator": "1.1.0", 16 | "@radix-ui/react-slot": "1.1.0", 17 | "@radix-ui/react-tooltip": "1.1.4", 18 | "class-variance-authority": "0.7.0", 19 | "clsx": "2.1.1", 20 | "lucide-react": "0.460.0", 21 | "next": "15.0.3", 22 | "react": "18.3.1", 23 | "react-dom": "18.3.1", 24 | "react-markdown": "9.0.1", 25 | "tailwind-merge": "2.5.4", 26 | "tailwindcss-animate": "1.0.7", 27 | "tailwindcss-jun-layout": "0.6.3" 28 | }, 29 | "devDependencies": { 30 | "@tailwindcss/typography": "0.5.15", 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-dom": "^18", 34 | "postcss": "^8", 35 | "tailwindcss": "^3.4.1", 36 | "typescript": "^5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/blog/NavTrigger.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { MenuIcon } from "lucide-react"; 4 | import { triggerEdgeDrawer } from "tailwindcss-jun-layout"; 5 | 6 | export default function NavTrigger() { 7 | return ( 8 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/blog/[slug]/TocTrigger.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { List } from "lucide-react"; 4 | import { triggerEdgeDrawerRight } from "tailwindcss-jun-layout"; 5 | 6 | export default function TocTrigger() { 7 | return ( 8 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { posts, type Post } from "@/app/blog/_data/posts"; 2 | import { notFound } from "next/navigation"; 3 | import { Badge } from "@/components/ui/badge"; 4 | import { Markdown } from "@/components/blog/Markdown"; 5 | import { TableOfContents } from "@/components/blog/TableOfContents"; 6 | import { menuGroups } from "@/app/dashboard/_data/menu"; 7 | import TocTrigger from "./TocTrigger"; 8 | 9 | interface BlogPostPageProps { 10 | params: Promise<{ 11 | slug: string; 12 | }>; 13 | } 14 | 15 | export function generateStaticParams() { 16 | return posts.map((post) => ({ 17 | slug: post.slug, 18 | })); 19 | } 20 | 21 | export default async function BlogPostPage({ params }: BlogPostPageProps) { 22 | const { slug } = await params; 23 | const post = posts.find((p: Post) => p.slug === slug); 24 | 25 | if (!post) { 26 | notFound(); 27 | } 28 | 29 | return ( 30 | <> 31 |
32 |
33 |
34 | 35 | {post.title} 40 | 41 |
42 |
43 |
44 |
45 | {post.tags.map((tag: string) => ( 46 | 50 | {tag} 51 | 52 | ))} 53 |
54 |

55 | {post.title} 56 |

57 |

{post.description}

58 | 65 |
66 |
67 |
68 |
69 | 70 |
71 |
72 |
73 | 74 |
75 |
76 | 77 |
78 |
79 |
80 | 81 |
82 |
83 |
84 |
85 | 86 | 87 |
88 |
89 |
90 | {menuGroups.map((group) => ( 91 |
92 |
{group.label}
93 |
    94 | {group.items.map((item) => { 95 | const Icon = item.icon; 96 | return ( 97 |
  • 98 | 102 |
  • 103 | ); 104 | })} 105 |
106 |
107 | ))} 108 |
109 |
110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/app/blog/_data/author.ts: -------------------------------------------------------------------------------- 1 | export interface SocialLink { 2 | platform: string; 3 | url: string; 4 | icon: string; 5 | } 6 | 7 | export interface Author { 8 | name: string; 9 | avatar: string; 10 | role: string; 11 | bio: string; 12 | location: string; 13 | company: string; 14 | socialLinks: SocialLink[]; 15 | skills: string[]; 16 | } 17 | 18 | export const author: Author = { 19 | name: "John Doe", 20 | avatar: 21 | "https://ui-avatars.com/api/?name=John+Doe&background=random&size=200", 22 | role: "Senior Software Engineer", 23 | bio: "Passionate about web development, open source, and teaching others. I write about JavaScript, TypeScript, React, and modern web development practices.", 24 | location: "San Francisco, CA", 25 | company: "TechCorp Inc.", 26 | socialLinks: [ 27 | { 28 | platform: "GitHub", 29 | url: "https://github.com/johndoe", 30 | icon: "github", 31 | }, 32 | { 33 | platform: "Twitter", 34 | url: "https://twitter.com/johndoe", 35 | icon: "twitter", 36 | }, 37 | { 38 | platform: "LinkedIn", 39 | url: "https://linkedin.com/in/johndoe", 40 | icon: "linkedin", 41 | }, 42 | ], 43 | skills: [ 44 | "JavaScript", 45 | "TypeScript", 46 | "React", 47 | "Next.js", 48 | "Node.js", 49 | "GraphQL", 50 | "AWS", 51 | "Docker", 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /src/app/blog/_data/posts.ts: -------------------------------------------------------------------------------- 1 | export interface TableOfContents { 2 | id: string; 3 | title: string; 4 | level: number; 5 | } 6 | 7 | export interface Post { 8 | id: string; 9 | slug: string; 10 | title: string; 11 | description: string; 12 | content: string; 13 | banner: string; 14 | publishedAt: string; 15 | tags: string[]; 16 | tableOfContents: TableOfContents[]; 17 | } 18 | 19 | export const posts: Post[] = [ 20 | { 21 | id: "1", 22 | slug: "getting-started-with-nextjs", 23 | title: "Getting Started with Next.js: A Comprehensive Guide", 24 | description: 25 | "Learn how to build modern web applications with Next.js, from setup to deployment.", 26 | content: ` 27 | # Getting Started with Next.js 28 | 29 | Next.js is a powerful React framework that makes building web applications a breeze. In this guide, we'll explore the fundamentals and best practices. 30 | 31 | ## Why Next.js? 32 | 33 | Next.js provides several key features out of the box: 34 | - Server-side rendering 35 | - Static site generation 36 | - API routes 37 | - File-based routing 38 | - Built-in CSS support 39 | 40 | ## Setting Up Your First Project 41 | 42 | To create a new Next.js project, run: 43 | 44 | \`\`\`bash 45 | npx create-next-app@latest my-app 46 | cd my-app 47 | npm run dev 48 | \`\`\` 49 | 50 | ## Project Structure 51 | 52 | A typical Next.js project includes: 53 | - \`pages/\`: Your application's pages 54 | - \`public/\`: Static assets 55 | - \`styles/\`: CSS files 56 | - \`components/\`: Reusable React components 57 | 58 | ## Best Practices 59 | 60 | 1. Use Static Generation when possible 61 | 2. Implement dynamic imports for better performance 62 | 3. Optimize images with next/image 63 | 4. Leverage API routes for backend functionality 64 | `, 65 | banner: 66 | "https://images.unsplash.com/photo-1555066931-4365d14bab8c?auto=format&fit=crop&w=2000&q=80", 67 | publishedAt: "2024-02-15", 68 | tags: ["Next.js", "React", "Web Development", "JavaScript"], 69 | tableOfContents: [ 70 | { id: "why-nextjs", title: "Why Next.js?", level: 2 }, 71 | { id: "setting-up", title: "Setting Up Your First Project", level: 2 }, 72 | { id: "project-structure", title: "Project Structure", level: 2 }, 73 | { id: "best-practices", title: "Best Practices", level: 2 }, 74 | ], 75 | }, 76 | { 77 | id: "2", 78 | slug: "mastering-typescript", 79 | title: "Mastering TypeScript: Types, Interfaces, and Best Practices", 80 | description: 81 | "A deep dive into TypeScript's type system and how to use it effectively in your projects.", 82 | content: ` 83 | # Mastering TypeScript 84 | 85 | TypeScript adds static typing to JavaScript, making your code more reliable and maintainable. Let's explore its key features. 86 | 87 | ## Type System Basics 88 | 89 | TypeScript's type system includes: 90 | - Basic types (string, number, boolean) 91 | - Arrays and tuples 92 | - Objects and interfaces 93 | - Unions and intersections 94 | 95 | ## Advanced Types 96 | 97 | Learn about advanced type features: 98 | - Generics 99 | - Utility types 100 | - Mapped types 101 | - Conditional types 102 | 103 | ## Best Practices 104 | 105 | 1. Enable strict mode 106 | 2. Use interfaces for object shapes 107 | 3. Leverage type inference 108 | 4. Document with JSDoc comments 109 | 110 | ## Real-World Examples 111 | 112 | Here's a practical example: 113 | 114 | \`\`\`typescript 115 | interface User { 116 | id: string; 117 | name: string; 118 | email: string; 119 | } 120 | 121 | function getUserById(id: string): Promise { 122 | return fetch(\`/api/users/\${id}\`).then(res => res.json()); 123 | } 124 | \`\`\` 125 | `, 126 | banner: 127 | "https://images.unsplash.com/photo-1587620962725-abab7fe55159?auto=format&fit=crop&w=2000&q=80", 128 | publishedAt: "2024-02-10", 129 | tags: ["TypeScript", "JavaScript", "Programming", "Web Development"], 130 | tableOfContents: [ 131 | { id: "type-system", title: "Type System Basics", level: 2 }, 132 | { id: "advanced-types", title: "Advanced Types", level: 2 }, 133 | { id: "best-practices", title: "Best Practices", level: 2 }, 134 | { id: "examples", title: "Real-World Examples", level: 2 }, 135 | ], 136 | }, 137 | ]; 138 | -------------------------------------------------------------------------------- /src/app/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BlogFooter } from "@/components/blog/BlogFooter"; 2 | import { Github, Twitter } from "lucide-react"; 3 | import NavTrigger from "./NavTrigger"; 4 | import Link from "next/link"; 5 | 6 | export default function BlogLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 |
13 |
14 |
15 |
16 | 17 | 39 | 40 | 60 |
61 |
62 |
63 | 68 | {children} 69 |
70 | 71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import { posts } from "@/app/blog/_data/posts"; 2 | import { author } from "@/app/blog/_data/author"; 3 | import { PostCard } from "@/components/blog/PostCard"; 4 | import { AuthorProfile } from "@/components/blog/AuthorProfile"; 5 | 6 | export default function BlogPage() { 7 | return ( 8 |
9 |
10 | {/* Author Profile Section */} 11 |
12 | 13 |
14 | 15 | {/* Blog Posts Section */} 16 |
17 |

Latest Posts

18 |
19 | {posts.map((post) => ( 20 | 21 | ))} 22 |
23 |
24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/chat/ChatItem.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 4 | import { Badge } from "@/components/ui/badge"; 5 | import type { Chat } from "./ChatList"; 6 | 7 | interface ChatItemProps { 8 | chat: Chat; 9 | selected?: boolean; 10 | onClick?: () => void; 11 | } 12 | 13 | export function ChatItem({ chat, selected, onClick }: ChatItemProps) { 14 | return ( 15 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/chat/ChatList.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollArea } from "@/components/ui/scroll-area"; 2 | import { 3 | Tooltip, 4 | TooltipTrigger, 5 | TooltipContent, 6 | } from "@/components/ui/tooltip"; 7 | import React from "react"; 8 | import { cn } from "@/lib/utils"; 9 | import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; 10 | import { Badge } from "@/components/ui/badge"; 11 | 12 | export interface Chat { 13 | id: string; 14 | name: string; 15 | lastMessage: string; 16 | timestamp: string; 17 | unread?: number; 18 | avatar?: string; 19 | isOnline?: boolean; 20 | } 21 | 22 | interface ChatListProps { 23 | chats: Chat[]; 24 | selectedId?: string; 25 | onSelect?: (id: string) => void; 26 | } 27 | 28 | export function ChatList({ chats, selectedId, onSelect }: ChatListProps) { 29 | const [sidebar, setSidebar] = React.useState(null); 30 | React.useEffect(() => { 31 | setSidebar(document.getElementById("chat-list-sidebar")); 32 | }, []); 33 | return ( 34 | 35 |
36 | {chats.map((chat) => ( 37 | 38 | 39 | 75 | 76 | 81 | {chat.name} 82 | 83 | 84 | ))} 85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/app/chat/_data/mock-chats.ts: -------------------------------------------------------------------------------- 1 | import type { Chat } from "@/components/chat/ChatList"; 2 | 3 | const messages = [ 4 | "Hey, how are you?", 5 | "Can we meet tomorrow?", 6 | "Did you see the latest update?", 7 | "Thanks for your help!", 8 | "Let's catch up soon!", 9 | ] as const; 10 | 11 | const timestamps = [ 12 | "just now", 13 | "5m ago", 14 | "10m ago", 15 | "1h ago", 16 | "2h ago", 17 | "yesterday", 18 | ] as const; 19 | 20 | export function createMockChats(): Chat[] { 21 | return Array.from({ length: 100 }, (_, i) => ({ 22 | id: `chat-${i + 1}`, 23 | name: `User ${i + 1}`, 24 | lastMessage: messages[i % messages.length], 25 | timestamp: timestamps[i % timestamps.length], 26 | unread: i % 3 === 0 ? (i % 5) + 1 : undefined, 27 | avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`, 28 | isOnline: i % 3 === 0, 29 | })); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/chat/_data/mock-messages.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from "@/components/chat/Conversation"; 2 | 3 | const messageContents = [ 4 | "Hey there! How's it going?", 5 | "I've been working on that project we discussed.", 6 | "Did you see the latest updates?", 7 | "Can we schedule a meeting for tomorrow?", 8 | "Thanks for your help earlier!", 9 | "The presentation went really well!", 10 | "I'll send you the documents soon.", 11 | "Have a great weekend!", 12 | "Let me know when you're free to chat.", 13 | "That sounds like a great idea!", 14 | ] as const; 15 | 16 | export function createMockMessages(): Message[] { 17 | return Array.from({ length: 100 }, (_, i) => { 18 | const isMe = i % 2 === 0; 19 | const minutesAgo = i * 3; 20 | const timestamp = new Date( 21 | Date.now() - minutesAgo * 60000 22 | ).toLocaleTimeString([], { 23 | hour: "2-digit", 24 | minute: "2-digit", 25 | }); 26 | 27 | return { 28 | id: `msg-${i + 1}`, 29 | content: messageContents[i % messageContents.length], 30 | timestamp, 31 | sender: { 32 | id: isMe ? "me" : "other", 33 | name: isMe ? "Me" : "John Doe", 34 | avatar: isMe 35 | ? undefined 36 | : `https://api.dicebear.com/7.x/avataaars/svg?seed=john`, 37 | }, 38 | isMe, 39 | }; 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/app/chat/_data/mock-settings.ts: -------------------------------------------------------------------------------- 1 | interface SharedMedia { 2 | images: string[]; 3 | files: { name: string; size: string }[]; 4 | } 5 | 6 | interface Participant { 7 | name: string; 8 | avatar?: string; 9 | status?: string; 10 | } 11 | 12 | export interface ChatSettings { 13 | participant: Participant; 14 | sharedMedia: SharedMedia; 15 | } 16 | 17 | export function createMockSettings(): ChatSettings { 18 | return { 19 | participant: { 20 | name: "John Doe", 21 | avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=john", 22 | status: "Last seen 2 hours ago", 23 | }, 24 | sharedMedia: { 25 | images: [ 26 | "https://picsum.photos/seed/1/200", 27 | "https://picsum.photos/seed/2/200", 28 | "https://picsum.photos/seed/3/200", 29 | "https://picsum.photos/seed/4/200", 30 | "https://picsum.photos/seed/5/200", 31 | "https://picsum.photos/seed/6/200", 32 | ], 33 | files: [ 34 | { 35 | name: "Project_Brief.pdf", 36 | size: "2.4 MB", 37 | }, 38 | { 39 | name: "Meeting_Notes.docx", 40 | size: "842 KB", 41 | }, 42 | { 43 | name: "Presentation.pptx", 44 | size: "4.1 MB", 45 | }, 46 | { 47 | name: "Budget_2024.xlsx", 48 | size: "1.2 MB", 49 | }, 50 | { 51 | name: "Design_Assets.zip", 52 | size: "15.8 MB", 53 | }, 54 | ], 55 | }, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ChatList } from "./ChatList"; 3 | import { Input } from "@/components/chat/Input"; 4 | import { createMockChats } from "./_data/mock-chats"; 5 | import { createMockMessages } from "./_data/mock-messages"; 6 | import { createMockSettings } from "./_data/mock-settings"; 7 | import { Conversation } from "@/components/chat/Conversation"; 8 | import { ChatSetting } from "@/components/chat/ChatSetting"; 9 | import { Button } from "@/components/ui/button"; 10 | import { 11 | UserPlus, 12 | MoreVertical, 13 | PanelRightClose, 14 | MenuSquare, 15 | X, 16 | PanelLeftOpen, 17 | PanelLeftClose, 18 | } from "lucide-react"; 19 | import { 20 | triggerEdgeCollapse, 21 | triggerEdgeCollapseRight, 22 | triggerEdgeDrawer, 23 | triggerEdgeDrawerRight, 24 | } from "tailwindcss-jun-layout"; 25 | import { TooltipProvider } from "@/components/ui/tooltip"; 26 | 27 | // Create mock data once 28 | const mockChats = createMockChats(); 29 | const mockMessages = createMockMessages(); 30 | const mockSettings = createMockSettings(); 31 | 32 | export default function ChatPage() { 33 | return ( 34 |
35 |
36 |
37 | 45 | 54 |

Messages

55 | 56 | {mockChats.length} conversations 57 | 58 |
59 |
60 | 63 | 72 | 80 |
81 |
82 | 83 | 84 | 100 | 101 | 102 |
103 | 104 |
105 | 106 | 132 | 133 |
134 | console.log("Sent message:", message)} /> 135 |
136 |
137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/app/dashboard/_data/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BarChart2, 3 | Calendar, 4 | Database, 5 | FileText, 6 | Home, 7 | Mail, 8 | Users, 9 | } from "lucide-react"; 10 | 11 | export const menuGroups = [ 12 | { 13 | label: "Overview", 14 | items: [ 15 | { 16 | icon: Home, 17 | label: "Dashboard", 18 | menus: [ 19 | { 20 | title: "History", 21 | url: "#", 22 | }, 23 | { 24 | title: "Starred", 25 | url: "#", 26 | }, 27 | { 28 | title: "Settings", 29 | url: "#", 30 | }, 31 | ], 32 | }, 33 | { icon: BarChart2, label: "Analytics" }, 34 | { icon: FileText, label: "Reports" }, 35 | ], 36 | }, 37 | { 38 | label: "Workspace", 39 | items: [ 40 | { icon: Mail, label: "Inbox" }, 41 | { icon: Calendar, label: "Calendar" }, 42 | { icon: Database, label: "Projects" }, 43 | { icon: Users, label: "Team" }, 44 | ], 45 | }, 46 | ]; 47 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChevronsUpDown, 3 | CommandIcon, 4 | MoreHorizontal, 5 | Plus, 6 | Settings, 7 | } from "lucide-react"; 8 | import { 9 | RailCollapse, 10 | TriggerLeftSidebarCollapse, 11 | TriggerMobileSidebar, 12 | } from "./triggers"; 13 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 14 | import AppSwitcher from "@/components/dashboard/AppSwitcher"; 15 | import UserSetting from "@/components/dashboard/UserSetting"; 16 | import CollapsibleMenu from "@/components/dashboard/CollapsibleMenu"; 17 | import { OrderTable } from "@/components/dashboard/OrderTable"; 18 | import { OrderStats } from "@/components/dashboard/OrderStats"; 19 | import { menuGroups } from "./_data/menu"; 20 | 21 | export default function Dashboard() { 22 | return ( 23 |
24 |
25 |
26 | 27 | 28 |

Playground Dashboard

29 |
30 |
31 | 32 | 145 | 146 |
147 |
148 |
149 | {menuGroups.map((group, index) => ( 150 |
151 |
152 | {group.label} 153 | 156 |
157 |
    158 | {group.items.map((item, itemIndex) => { 159 | const Icon = item.icon; 160 | return ( 161 |
  • 162 | 166 | 169 | {item.menus && ( 170 |
    171 |
    172 |
      173 | {item.menus.map((item, subIndex) => ( 174 |
    • 178 | 183 |
    • 184 | ))} 185 |
    186 |
    187 |
    188 | )} 189 |
  • 190 | ); 191 | })} 192 |
193 |
194 | ))} 195 |
196 |
197 |
198 | 199 | 200 |
201 |
202 |
203 | ); 204 | } 205 | 206 | function MiniApp() { 207 | return ( 208 |
209 |
Subheader
210 | 213 |
App content
214 |
215 | ); 216 | } 217 | -------------------------------------------------------------------------------- /src/app/dashboard/triggers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Menu, PanelLeftClose, PanelRightClose } from "lucide-react"; 3 | import { triggerEdgeCollapse, triggerEdgeDrawer } from "tailwindcss-jun-layout"; 4 | 5 | export const TriggerMobileSidebar = () => ( 6 | 9 | ); 10 | 11 | export const RailCollapse = () => ( 12 | 31 | ); 32 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/React-in-Thai/learn-tailwindcss-jun-layout/74106a2144db48c68f434556fddee39985c210f2/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/React-in-Thai/learn-tailwindcss-jun-layout/74106a2144db48c68f434556fddee39985c210f2/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/React-in-Thai/learn-tailwindcss-jun-layout/74106a2144db48c68f434556fddee39985c210f2/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 240 10% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 240 10% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 240 10% 3.9%; 17 | --primary: 240 5.9% 10%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | --muted: 240 4.8% 95.9%; 22 | --muted-foreground: 240 3.8% 46.1%; 23 | --accent: 240 4.8% 95.9%; 24 | --accent-foreground: 240 5.9% 10%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 240 5.9% 90%; 28 | --input: 240 5.9% 90%; 29 | --ring: 240 10% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | --sidebar-background: 0 0% 98%; 37 | --sidebar-foreground: 240 5.3% 26.1%; 38 | --sidebar-primary: 240 5.9% 10%; 39 | --sidebar-primary-foreground: 0 0% 98%; 40 | --sidebar-accent: 240 4.8% 95.9%; 41 | --sidebar-accent-foreground: 240 5.9% 10%; 42 | --sidebar-border: 220 13% 91%; 43 | --sidebar-ring: 217.2 91.2% 59.8%; 44 | } 45 | .dark { 46 | --background: 240 10% 3.9%; 47 | --foreground: 0 0% 98%; 48 | --card: 240 10% 3.9%; 49 | --card-foreground: 0 0% 98%; 50 | --popover: 240 10% 3.9%; 51 | --popover-foreground: 0 0% 98%; 52 | --primary: 0 0% 98%; 53 | --primary-foreground: 240 5.9% 10%; 54 | --secondary: 240 3.7% 15.9%; 55 | --secondary-foreground: 0 0% 98%; 56 | --muted: 240 3.7% 15.9%; 57 | --muted-foreground: 240 5% 64.9%; 58 | --accent: 240 3.7% 15.9%; 59 | --accent-foreground: 0 0% 98%; 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | --border: 240 3.7% 15.9%; 63 | --input: 240 3.7% 15.9%; 64 | --ring: 240 4.9% 83.9%; 65 | --chart-1: 220 70% 50%; 66 | --chart-2: 160 60% 45%; 67 | --chart-3: 30 80% 55%; 68 | --chart-4: 280 65% 60%; 69 | --chart-5: 340 75% 55%; 70 | --sidebar-background: 240 5.9% 10%; 71 | --sidebar-foreground: 240 4.8% 95.9%; 72 | --sidebar-primary: 224.3 76.3% 48%; 73 | --sidebar-primary-foreground: 0 0% 100%; 74 | --sidebar-accent: 240 3.7% 15.9%; 75 | --sidebar-accent-foreground: 240 4.8% 95.9%; 76 | --sidebar-border: 240 3.7% 15.9%; 77 | --sidebar-ring: 217.2 91.2% 59.8%; 78 | } 79 | } 80 | 81 | @layer base { 82 | * { 83 | @apply border-border; 84 | } 85 | body { 86 | @apply bg-background text-foreground; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Create Next App", 18 | description: "Generated by create next app", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardHeader, 4 | CardTitle, 5 | CardDescription, 6 | CardFooter, 7 | } from "@/components/ui/card"; 8 | import { ArrowRight } from "lucide-react"; 9 | import Link from "next/link"; 10 | 11 | export default function Home() { 12 | return ( 13 |
14 |
15 |

Layout Demos

16 | 17 |
18 | 19 | 23 | 24 | Dashboard 25 | 26 | 27 | 28 | A comprehensive overview of your system metrics, key performance 29 | indicators, and real-time analytics. Monitor activities, track 30 | progress, and make data-driven decisions from a centralized 31 | control center. 32 | 33 | 34 | 35 | View Dashboard 36 | 37 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | Chat 48 | 49 | 50 | 51 | Real-time messaging platform with a modern interface. Connect with 52 | users, share files, and collaborate effectively through individual 53 | and group conversations. 54 | 55 | 56 | 57 | Open Chat 58 | 59 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | Blog 70 | 71 | 72 | 73 | A personal blog showcasing thoughts, tutorials, and insights about 74 | web development, programming best practices, and the latest 75 | technology trends. 76 | 77 | 78 | 79 | Read Blog 80 | 81 | 82 | 83 | 84 |
85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/app/workshop/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { posts, type Post } from "@/app/blog/_data/posts"; 2 | import { notFound } from "next/navigation"; 3 | import { Badge } from "@/components/ui/badge"; 4 | import { Markdown } from "@/components/blog/Markdown"; 5 | import { TableOfContents } from "@/components/blog/TableOfContents"; 6 | import { BlogFooter } from "@/components/blog/BlogFooter"; 7 | 8 | interface BlogPostPageProps { 9 | params: Promise<{ 10 | slug: string; 11 | }>; 12 | } 13 | 14 | export function generateStaticParams() { 15 | return posts.map((post) => ({ 16 | slug: post.slug, 17 | })); 18 | } 19 | 20 | export default async function BlogPostPage({ params }: BlogPostPageProps) { 21 | const { slug } = await params; 22 | const post = posts.find((p: Post) => p.slug === slug); 23 | 24 | if (!post) { 25 | notFound(); 26 | } 27 | 28 | return ( 29 |
30 |
31 |
32 |
33 | 34 | {post.title} 39 | 40 |
41 |
42 |
43 |
44 | {post.tags.map((tag: string) => ( 45 | 49 | {tag} 50 | 51 | ))} 52 |
53 |

54 | {post.title} 55 |

56 |

{post.description}

57 | 64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 | {/* Main Content */} 72 |
73 |
74 | 75 |
76 |
77 | 78 | {/* Table of Contents Sidebar */} 79 |
80 |
81 | 82 |
83 |
84 |
85 |
86 |
87 | 88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/app/workshop/blog/guide.md: -------------------------------------------------------------------------------- 1 | # Dashboard layout workshop 2 | 3 | - Run `npm install && npm run dev` 4 | - Open http://localhost:3333/workshop/blog/ 5 | 6 | If you see the blog, you are ready to start. 7 | 8 | The blog is generated from AI without using Tailwind Jun Layout. Your job is to apply Tailwind Jun Layout to the current codebase. 9 | 10 | Feel free to add your creativity on top of the tasks. 11 | 12 | ## Tasks 13 | 14 | - [ ] Add layout structure classes to the page 15 | - [ ] Add a header 16 | - [ ] Add navigation (mockup links) 17 | - [ ] Hide the navigation on mobile and use EdgeSidebar drawer mode instead 18 | - [ ] Apply InsetSidebar to the table of contents in blog post detail page 19 | - [ ] Change between `sticky` (default), `fixed`, and `absolute` to see the difference 20 | - [ ] Hide InsetSidebar on mobile and render table of content on EdgeSidebar drawer instead. 21 | - [ ] Move shared components to `src/app/blog/layout.tsx` 22 | -------------------------------------------------------------------------------- /src/app/workshop/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import { posts } from "@/app/blog/_data/posts"; 2 | import { author } from "@/app/blog/_data/author"; 3 | import { PostCard } from "@/components/blog/PostCard"; 4 | import { AuthorProfile } from "@/components/blog/AuthorProfile"; 5 | import { BlogFooter } from "@/components/blog/BlogFooter"; 6 | 7 | export default function BlogPage() { 8 | return ( 9 |
10 |
11 |
12 | {/* Author Profile Section */} 13 |
14 | 15 |
16 | 17 | {/* Blog Posts Section */} 18 |
19 |

Latest Posts

20 |
21 | {posts.map((post) => ( 22 | 23 | ))} 24 |
25 |
26 |
27 |
28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/workshop/chat/guide.md: -------------------------------------------------------------------------------- 1 | # Chat layout workshop 2 | 3 | - Run `npm install && npm run dev` 4 | - Open http://localhost:3333/workshop/chat/ 5 | 6 | If you see a chat app, you are ready to start. 7 | 8 | The chat app is built with Tailwind Jun Layout but still need a lot of fine tuning. Complete all the tasks below using all the things you have learned from the dashboard layout workshop. 9 | 10 | Feel free to add your creativity on top of the tasks. 11 | 12 | ## Tasks 13 | 14 | - [ ] standalone layout to keep the footer on the screen 15 | - [ ] add Rail and Trigger (at header) to collapse left-sidebar to `80px` 16 | - [ ] use different icons between collapse/uncollapse states 17 | - [ ] hide the chat list on mobile and open it as a drawer 18 | - [ ] hide the right sidebar and open it when clicking the "more" button at the end of the header 19 | - [ ] change the icon from dot to close when the right sidebar open 20 | - [ ] Make the chat list displays proportionally when the left sidebar collapsed 21 | - [ ] Hide name 22 | - [ ] Show name using tooltip 23 | -------------------------------------------------------------------------------- /src/app/workshop/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ChatList } from "@/components/chat/ChatList"; 3 | import { Input } from "@/components/chat/Input"; 4 | import { createMockChats } from "@/app/chat/_data/mock-chats"; 5 | import { createMockMessages } from "@/app/chat/_data/mock-messages"; 6 | import { createMockSettings } from "@/app/chat/_data/mock-settings"; 7 | import { Conversation } from "@/components/chat/Conversation"; 8 | import { ChatSetting } from "@/components/chat/ChatSetting"; 9 | import { Button } from "@/components/ui/button"; 10 | import { UserPlus, MoreVertical } from "lucide-react"; 11 | 12 | // Create mock data once 13 | const mockChats = createMockChats(); 14 | const mockMessages = createMockMessages(); 15 | const mockSettings = createMockSettings(); 16 | 17 | export default function ChatPage() { 18 | return ( 19 |
20 |
21 |
22 |

Messages

23 | 24 | {mockChats.length} conversations 25 | 26 |
27 |
28 | 31 | 34 |
35 |
36 | 37 | 45 | 46 |
47 | 48 |
49 | 50 | 58 | 59 |
60 | console.log("Sent message:", message)} /> 61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/app/workshop/dashboard/guide.md: -------------------------------------------------------------------------------- 1 | # Dashboard layout workshop 2 | 3 | - Run `npm install && npm run dev` 4 | - Open http://localhost:3333/workshop/dashboard/ 5 | 6 | If you see a text "Edit me...", you are ready to start. 7 | 8 | ## Tasks 9 | 10 | - [ ] Layout structure (Header, Content, EdgeSidebar) 11 | - [ ] Build and check the CSS output 12 | - Edge sidebar 13 | - [ ] changing width 14 | - [ ] trigger collapse & icon 15 | - [ ] collapsed width 16 | - [ ] hover to expand 17 | - [ ] auto collapse 18 | - [ ] drawer mode (mobile) & trigger 19 | - Sidebar elements 20 | - [ ] sidebar container 21 | - [ ] list of menus 22 | - [ ] group of menu list 23 | - [ ] menu action 24 | - [ ] handle collapsed state with icon and text 25 | - [ ] icon shrink size 26 | - [ ] group of texts 27 | - [ ] collapsible menu 28 | - [ ] tooltip 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/app/workshop/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { menuGroups } from "@/app/dashboard/_data/menu"; 2 | 3 | export default function WorkshopDashboard() { 4 | return
Edit me...
; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/blog/AuthorProfile.tsx: -------------------------------------------------------------------------------- 1 | import { Author, SocialLink } from "@/app/blog/_data/author"; 2 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 3 | import { Badge } from "@/components/ui/badge"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Github, Twitter, Linkedin, MapPin, Building2 } from "lucide-react"; 6 | 7 | const iconMap = { 8 | github: Github, 9 | twitter: Twitter, 10 | linkedin: Linkedin, 11 | }; 12 | 13 | interface AuthorProfileProps { 14 | author: Author; 15 | } 16 | 17 | export function AuthorProfile({ author }: AuthorProfileProps) { 18 | return ( 19 |
20 | 21 | 22 | {author.name.slice(0, 2)} 23 | 24 | 25 |
26 |

{author.name}

27 |

{author.role}

28 |
29 | 30 |
31 |
32 | 33 | {author.location} 34 |
35 |
36 | 37 | {author.company} 38 |
39 |
40 | 41 |

{author.bio}

42 | 43 |
44 | {author.socialLinks.map((link: SocialLink) => { 45 | const Icon = iconMap[link.icon as keyof typeof iconMap]; 46 | return ( 47 | 53 | ); 54 | })} 55 |
56 | 57 |
58 | {author.skills.map((skill: string) => ( 59 | 60 | {skill} 61 | 62 | ))} 63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/blog/BlogFooter.tsx: -------------------------------------------------------------------------------- 1 | import { Github, Twitter, Linkedin, Mail, Rss } from "lucide-react"; 2 | import Link from "next/link"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Separator } from "@/components/ui/separator"; 5 | import { NewsletterForm } from "./NewsletterForm"; 6 | 7 | const footerLinks = { 8 | resources: [ 9 | { label: "Documentation", href: "#" }, 10 | { label: "Blog Posts", href: "/blog" }, 11 | { label: "Tutorials", href: "#" }, 12 | { label: "Code Examples", href: "#" }, 13 | { label: "GitHub Repos", href: "#" }, 14 | ], 15 | community: [ 16 | { label: "Discord Server", href: "#" }, 17 | { label: "Twitter Community", href: "#" }, 18 | { label: "GitHub Discussions", href: "#" }, 19 | { label: "Stack Overflow", href: "#" }, 20 | { label: "Contributing Guide", href: "#" }, 21 | ], 22 | legal: [ 23 | { label: "Privacy Policy", href: "#" }, 24 | { label: "Terms of Service", href: "#" }, 25 | { label: "Cookie Policy", href: "#" }, 26 | { label: "License", href: "#" }, 27 | ], 28 | topics: [ 29 | { label: "Web Development", href: "#" }, 30 | { label: "JavaScript", href: "#" }, 31 | { label: "TypeScript", href: "#" }, 32 | { label: "React", href: "#" }, 33 | { label: "Next.js", href: "#" }, 34 | { label: "Node.js", href: "#" }, 35 | ], 36 | }; 37 | 38 | const socialLinks = [ 39 | { icon: Github, href: "#", label: "GitHub" }, 40 | { icon: Twitter, href: "#", label: "Twitter" }, 41 | { icon: Linkedin, href: "#", label: "LinkedIn" }, 42 | { icon: Mail, href: "#", label: "Email" }, 43 | { icon: Rss, href: "#", label: "RSS" }, 44 | ]; 45 | 46 | export function BlogFooter() { 47 | return ( 48 |
49 |
50 | {/* Newsletter Section */} 51 |
52 |
53 |
54 |

55 | Subscribe to the newsletter 56 |

57 |

58 | Get notifications about new blog posts, tutorials, and updates 59 | directly to your inbox. 60 |

61 | 62 |
63 |
64 |
65 | 66 | {/* Links Section */} 67 |
68 |
69 |

Resources

70 |
    71 | {footerLinks.resources.map((link) => ( 72 |
  • 73 | 77 | {link.label} 78 | 79 |
  • 80 | ))} 81 |
82 |
83 | 84 |
85 |

Community

86 |
    87 | {footerLinks.community.map((link) => ( 88 |
  • 89 | 93 | {link.label} 94 | 95 |
  • 96 | ))} 97 |
98 |
99 | 100 |
101 |

Topics

102 |
    103 | {footerLinks.topics.map((link) => ( 104 |
  • 105 | 109 | {link.label} 110 | 111 |
  • 112 | ))} 113 |
114 |
115 | 116 |
117 |

Legal

118 |
    119 | {footerLinks.legal.map((link) => ( 120 |
  • 121 | 125 | {link.label} 126 | 127 |
  • 128 | ))} 129 |
130 |
131 |
132 | 133 | 134 | 135 | {/* Bottom Section */} 136 |
137 |
138 |

Layout Workshop

139 |

140 | © {new Date().getFullYear()} All rights reserved. 141 |

142 |
143 | 144 |
145 | {socialLinks.map((link) => { 146 | const Icon = link.icon; 147 | return ( 148 | 159 | ); 160 | })} 161 |
162 |
163 |
164 |
165 | ); 166 | } 167 | -------------------------------------------------------------------------------- /src/components/blog/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from "react-markdown"; 2 | 3 | interface MarkdownProps { 4 | content: string; 5 | } 6 | 7 | export function Markdown({ content }: MarkdownProps) { 8 | return {content}; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/blog/NewsletterForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Input } from "@/components/ui/input"; 4 | 5 | export function NewsletterForm() { 6 | return ( 7 |
e.preventDefault()}> 8 | 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/blog/PostCard.tsx: -------------------------------------------------------------------------------- 1 | import { Post } from "@/app/blog/_data/posts"; 2 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 3 | import { Badge } from "@/components/ui/badge"; 4 | import Link from "next/link"; 5 | 6 | interface PostCardProps { 7 | post: Post; 8 | } 9 | 10 | export function PostCard({ post }: PostCardProps) { 11 | return ( 12 | 13 | 14 |
15 | {post.title} 20 |
21 | 22 |
23 |
24 | {post.tags.map((tag: string) => ( 25 | 26 | {tag} 27 | 28 | ))} 29 |
30 |

31 | {post.title} 32 |

33 |

{post.description}

34 |
35 |
36 | 37 | 44 | 45 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/blog/TableOfContents.tsx: -------------------------------------------------------------------------------- 1 | import { TableOfContents as TOCItem } from "@/app/blog/_data/posts"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface TableOfContentsProps { 5 | items: TOCItem[]; 6 | } 7 | 8 | export function TableOfContents({ items }: TableOfContentsProps) { 9 | return ( 10 |
11 |

Table of Contents

12 |
13 | {items.map((item) => ( 14 | 23 | {item.title} 24 | 25 | ))} 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/chat/ChatItem.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 4 | import { Badge } from "@/components/ui/badge"; 5 | import type { Chat } from "./ChatList"; 6 | 7 | interface ChatItemProps { 8 | chat: Chat; 9 | selected?: boolean; 10 | onClick?: () => void; 11 | } 12 | 13 | export function ChatItem({ chat, selected, onClick }: ChatItemProps) { 14 | return ( 15 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/chat/ChatList.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollArea } from "@/components/ui/scroll-area"; 2 | import { ChatItem } from "@/components/chat/ChatItem"; 3 | 4 | export interface Chat { 5 | id: string; 6 | name: string; 7 | lastMessage: string; 8 | timestamp: string; 9 | unread?: number; 10 | avatar?: string; 11 | isOnline?: boolean; 12 | } 13 | 14 | interface ChatListProps { 15 | chats: Chat[]; 16 | selectedId?: string; 17 | onSelect?: (id: string) => void; 18 | } 19 | 20 | export function ChatList({ chats, selectedId, onSelect }: ChatListProps) { 21 | return ( 22 | 23 |
24 | {chats.map((chat) => ( 25 | onSelect?.(chat.id)} 30 | /> 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/chat/ChatSetting.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollArea } from "@/components/ui/scroll-area"; 2 | import { Separator } from "@/components/ui/separator"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Bell, 6 | Image as ImageIcon, 7 | Link, 8 | MoreVertical, 9 | UserPlus, 10 | } from "lucide-react"; 11 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 12 | 13 | interface ChatSettingProps { 14 | participant: { 15 | name: string; 16 | avatar?: string; 17 | status?: string; 18 | }; 19 | sharedMedia: { 20 | images: string[]; 21 | files: { name: string; size: string }[]; 22 | }; 23 | } 24 | 25 | export function ChatSetting({ participant, sharedMedia }: ChatSettingProps) { 26 | return ( 27 | 28 |
29 |
30 | 31 | 32 | {participant.name.slice(0, 2)} 33 | 34 |

{participant.name}

35 |

{participant.status}

36 |
37 | 38 |
39 | 42 | 45 | 48 | 51 |
52 | 53 | 54 | 55 |
56 |

Shared Media

57 |
58 | {sharedMedia.images.map((image, i) => ( 59 |
63 | 68 |
69 | ))} 70 |
71 |
72 | 73 | 74 | 75 |
76 |

Shared Files

77 |
78 | {sharedMedia.files.map((file, i) => ( 79 |
83 | 84 |
85 |

{file.name}

86 |

{file.size}

87 |
88 |
89 | ))} 90 |
91 |
92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/components/chat/Conversation.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 | import { ScrollArea } from "@/components/ui/scroll-area"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface Message { 6 | id: string; 7 | content: string; 8 | timestamp: string; 9 | sender: { 10 | id: string; 11 | name: string; 12 | avatar?: string; 13 | }; 14 | isMe: boolean; 15 | } 16 | 17 | interface ConversationProps { 18 | messages: Message[]; 19 | } 20 | 21 | export function Conversation({ messages }: ConversationProps) { 22 | return ( 23 | 24 |
25 | {messages.map((message) => ( 26 |
33 | 34 | 35 | {message.sender.name.slice(0, 2)} 36 | 37 |
38 |
46 | {message.content} 47 |
48 | 49 | {message.timestamp} 50 | 51 |
52 |
53 | ))} 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/chat/Input.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Textarea } from "@/components/ui/textarea"; 3 | import { Paperclip, Smile, Image as ImageIcon, Send, Mic } from "lucide-react"; 4 | import { useState, KeyboardEvent } from "react"; 5 | 6 | interface InputProps { 7 | onSend: (message: string) => void; 8 | } 9 | 10 | export function Input({ onSend }: InputProps) { 11 | const [message, setMessage] = useState(""); 12 | 13 | const handleKeyDown = (e: KeyboardEvent) => { 14 | if (e.key === "Enter" && !e.shiftKey) { 15 | e.preventDefault(); 16 | if (message.trim()) { 17 | onSend(message); 18 | setMessage(""); 19 | } 20 | } 21 | }; 22 | 23 | return ( 24 |
25 |
26 |
27 | 30 | 33 |
34 |