Initial commit: Next.js migration with SSG for GEO optimization
- Migrated from React SPA to Next.js with static site generation
- All 13 pages converted to pre-rendered HTML
- JSON-LD schema embedded in every page
- Optimized for Generative Engine Optimization (GEO)
- Deployed to Netlify at kbanc-nextjs.netlify.app
🤖 Generated with Claude Code
|
|
@ -0,0 +1,37 @@
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
.env
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# netlify
|
||||||
|
.netlify
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
# Custom Domain Setup: kbanc.com → Netlify
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
- ✅ Site deployed to Netlify: https://kbanc-nextjs.netlify.app
|
||||||
|
- 🔄 Domain transferring from Squarespace to Namecheap
|
||||||
|
- ⏳ Waiting to configure DNS
|
||||||
|
|
||||||
|
## Step 1: Add Domain in Netlify (Do Now)
|
||||||
|
|
||||||
|
### 1.1 Navigate to Domain Settings
|
||||||
|
1. Go to: https://app.netlify.com/projects/kbanc-nextjs/settings/domain
|
||||||
|
2. Or: Netlify Dashboard → Sites → kbanc-nextjs → Domain management
|
||||||
|
|
||||||
|
### 1.2 Add Primary Domain
|
||||||
|
1. Click "**Add domain alias**" or "**Add custom domain**"
|
||||||
|
2. Enter: `kbanc.com`
|
||||||
|
3. Click "**Verify**" → Netlify will check if you own it
|
||||||
|
4. If asked "Add domain?", click "**Yes, add domain**"
|
||||||
|
|
||||||
|
### 1.3 Add WWW Subdomain
|
||||||
|
1. Click "**Add domain alias**" again
|
||||||
|
2. Enter: `www.kbanc.com`
|
||||||
|
3. Click "**Verify**" and confirm
|
||||||
|
|
||||||
|
### 1.4 Set Primary Domain
|
||||||
|
1. Find `kbanc.com` in the list
|
||||||
|
2. Click the three dots (⋯) → "**Set as primary domain**"
|
||||||
|
3. This makes kbanc.com the main URL (www will redirect to it)
|
||||||
|
|
||||||
|
## Step 2: Wait for Namecheap Transfer (In Progress)
|
||||||
|
|
||||||
|
You'll receive an email from Namecheap when the transfer completes. This typically takes:
|
||||||
|
- **From Squarespace:** 5-7 days
|
||||||
|
- **Status check:** Log into Namecheap → Domain List
|
||||||
|
|
||||||
|
## Step 3: Configure DNS in Namecheap (After Transfer)
|
||||||
|
|
||||||
|
### Option A: Netlify DNS (Recommended ⭐)
|
||||||
|
|
||||||
|
**Why this option:**
|
||||||
|
- ✅ Fastest and simplest
|
||||||
|
- ✅ Automatic SSL certificates
|
||||||
|
- ✅ Netlify manages everything
|
||||||
|
- ✅ Best performance
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **Get Netlify Nameservers:**
|
||||||
|
- Go to: https://app.netlify.com/projects/kbanc-nextjs/settings/domain
|
||||||
|
- Look for "Nameservers" section
|
||||||
|
- You should see 4 nameservers like:
|
||||||
|
```
|
||||||
|
dns1.p01.nsone.net
|
||||||
|
dns2.p01.nsone.net
|
||||||
|
dns3.p01.nsone.net
|
||||||
|
dns4.p01.nsone.net
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update Namecheap Nameservers:**
|
||||||
|
- Log into Namecheap: https://ap.www.namecheap.com/
|
||||||
|
- Go to: Domain List → Select kbanc.com
|
||||||
|
- Click "Manage"
|
||||||
|
- Find "Nameservers" section
|
||||||
|
- Select "**Custom DNS**"
|
||||||
|
- Enter all 4 Netlify nameservers (one per field)
|
||||||
|
- Click "**Save**" (green checkmark)
|
||||||
|
|
||||||
|
3. **Wait for propagation:** 5 minutes to 48 hours (usually ~1 hour)
|
||||||
|
|
||||||
|
### Option B: Namecheap DNS (More Manual)
|
||||||
|
|
||||||
|
**Why this option:**
|
||||||
|
- ✅ Keep all DNS in one place
|
||||||
|
- ✅ Can add other records (email, etc.)
|
||||||
|
- ⚠️ Requires manual SSL setup
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **Log into Namecheap:**
|
||||||
|
- Go to: https://ap.www.namecheap.com/
|
||||||
|
- Domain List → Select kbanc.com → Manage
|
||||||
|
|
||||||
|
2. **Go to Advanced DNS:**
|
||||||
|
- Click "**Advanced DNS**" tab
|
||||||
|
|
||||||
|
3. **Delete existing records:**
|
||||||
|
- Remove any existing A, CNAME, or ALIAS records for @ and www
|
||||||
|
|
||||||
|
4. **Add A Record (for apex domain kbanc.com):**
|
||||||
|
```
|
||||||
|
Type: A Record
|
||||||
|
Host: @
|
||||||
|
Value: 75.2.60.5
|
||||||
|
TTL: Automatic (or 300)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Add CNAME Record (for www.kbanc.com):**
|
||||||
|
```
|
||||||
|
Type: CNAME Record
|
||||||
|
Host: www
|
||||||
|
Value: kbanc-nextjs.netlify.app
|
||||||
|
TTL: Automatic (or 300)
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Save all changes**
|
||||||
|
|
||||||
|
7. **Wait for propagation:** 5 minutes to 48 hours (usually ~1 hour)
|
||||||
|
|
||||||
|
## Step 4: Verify DNS Propagation
|
||||||
|
|
||||||
|
### Check DNS Status:
|
||||||
|
```bash
|
||||||
|
# Check apex domain
|
||||||
|
dig kbanc.com
|
||||||
|
|
||||||
|
# Check www subdomain
|
||||||
|
dig www.kbanc.com
|
||||||
|
|
||||||
|
# Check from multiple locations
|
||||||
|
https://www.whatsmydns.net/#A/kbanc.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### What you should see:
|
||||||
|
- **kbanc.com** → Points to `75.2.60.5` (Netlify IP)
|
||||||
|
- **www.kbanc.com** → CNAME to `kbanc-nextjs.netlify.app`
|
||||||
|
|
||||||
|
## Step 5: Enable HTTPS (Automatic)
|
||||||
|
|
||||||
|
Once DNS propagates:
|
||||||
|
|
||||||
|
1. **Netlify provisions SSL automatically:**
|
||||||
|
- Go to: https://app.netlify.com/projects/kbanc-nextjs/settings/domain
|
||||||
|
- Look for "HTTPS" section
|
||||||
|
- Should say "Certificate pending" → "Certificate active" (within 1 hour)
|
||||||
|
|
||||||
|
2. **Enable Force HTTPS:**
|
||||||
|
- In same section, toggle "**Force HTTPS**" to ON
|
||||||
|
- This redirects http:// to https://
|
||||||
|
|
||||||
|
3. **Test your site:**
|
||||||
|
- https://kbanc.com (should load your site)
|
||||||
|
- http://kbanc.com (should redirect to https)
|
||||||
|
- https://www.kbanc.com (should redirect to https://kbanc.com)
|
||||||
|
|
||||||
|
## Step 6: Update Old Site (Optional)
|
||||||
|
|
||||||
|
If you want to keep your old Lovable site as backup:
|
||||||
|
|
||||||
|
1. **Add redirect from old site:**
|
||||||
|
Create a redirect from kbanc.lovable.app → kbanc.com
|
||||||
|
|
||||||
|
2. **Or keep both:**
|
||||||
|
- New site: kbanc.com (Next.js on Netlify)
|
||||||
|
- Old site: kbanc.lovable.app (React SPA)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### DNS not propagating?
|
||||||
|
**Check:**
|
||||||
|
```bash
|
||||||
|
dig kbanc.com +short
|
||||||
|
```
|
||||||
|
**Should return:** `75.2.60.5`
|
||||||
|
|
||||||
|
**If not:**
|
||||||
|
- Wait longer (can take up to 48 hours)
|
||||||
|
- Clear your DNS cache: `sudo dscacheutil -flushcache`
|
||||||
|
- Check Namecheap Advanced DNS settings
|
||||||
|
|
||||||
|
### SSL certificate not provisioning?
|
||||||
|
**Common causes:**
|
||||||
|
- DNS not fully propagated yet (wait longer)
|
||||||
|
- CAA records blocking Let's Encrypt (remove any CAA records)
|
||||||
|
- Domain not verified in Netlify
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Go to: https://app.netlify.com/projects/kbanc-nextjs/settings/domain
|
||||||
|
2. Click "Verify DNS configuration"
|
||||||
|
3. If issues, click "Renew certificate"
|
||||||
|
|
||||||
|
### Site showing 404?
|
||||||
|
**Check:**
|
||||||
|
1. DNS propagated? (use whatsmydns.net)
|
||||||
|
2. Domain added in Netlify? (check domain settings)
|
||||||
|
3. Site deployed? (check deploys page)
|
||||||
|
|
||||||
|
### www not redirecting?
|
||||||
|
**In Netlify:**
|
||||||
|
1. Domain Settings → Find www.kbanc.com
|
||||||
|
2. It should say "Redirects to kbanc.com"
|
||||||
|
3. If not, set kbanc.com as primary domain
|
||||||
|
|
||||||
|
## DNS Record Reference
|
||||||
|
|
||||||
|
### Using Netlify DNS (Nameservers)
|
||||||
|
```
|
||||||
|
Nameserver 1: dns1.p01.nsone.net
|
||||||
|
Nameserver 2: dns2.p01.nsone.net
|
||||||
|
Nameserver 3: dns3.p01.nsone.net
|
||||||
|
Nameserver 4: dns4.p01.nsone.net
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Namecheap DNS (A/CNAME)
|
||||||
|
```
|
||||||
|
@ (apex) A 75.2.60.5
|
||||||
|
www CNAME kbanc-nextjs.netlify.app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
| Step | Action | Time |
|
||||||
|
|------|--------|------|
|
||||||
|
| 1 | Add domain in Netlify | 2 minutes |
|
||||||
|
| 2 | Wait for Namecheap transfer | 5-7 days |
|
||||||
|
| 3 | Update DNS in Namecheap | 5 minutes |
|
||||||
|
| 4 | DNS propagation | 5 min - 48 hrs |
|
||||||
|
| 5 | SSL certificate issued | Automatic |
|
||||||
|
| 6 | Site live at kbanc.com | ✅ Done! |
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Current deployment:**
|
||||||
|
- Netlify URL: https://kbanc-nextjs.netlify.app
|
||||||
|
- Admin: https://app.netlify.com/projects/kbanc-nextjs
|
||||||
|
|
||||||
|
**Target setup:**
|
||||||
|
- Primary: https://kbanc.com
|
||||||
|
- Redirect: https://www.kbanc.com → https://kbanc.com
|
||||||
|
- Old site: https://kbanc.lovable.app (optional backup)
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
**Netlify Support Docs:**
|
||||||
|
- Custom domains: https://docs.netlify.com/domains-https/custom-domains/
|
||||||
|
- DNS configuration: https://docs.netlify.com/domains-https/netlify-dns/
|
||||||
|
- HTTPS/SSL: https://docs.netlify.com/domains-https/https-ssl/
|
||||||
|
|
||||||
|
**Check Status:**
|
||||||
|
- DNS propagation: https://www.whatsmydns.net
|
||||||
|
- SSL status: https://www.ssllabs.com/ssltest/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Summary:**
|
||||||
|
1. ✅ Add kbanc.com to Netlify (now)
|
||||||
|
2. ⏳ Wait for Namecheap transfer (5-7 days)
|
||||||
|
3. ⚙️ Update nameservers in Namecheap (5 minutes)
|
||||||
|
4. ⏱️ Wait for DNS propagation (~1 hour)
|
||||||
|
5. 🔒 SSL auto-enables
|
||||||
|
6. ✨ Site live at kbanc.com!
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
# Next.js Migration Complete ✅
|
||||||
|
|
||||||
|
## What Was Done
|
||||||
|
|
||||||
|
Successfully migrated your React SPA to Next.js with Static Site Generation (SSG) for optimal **Generative Engine Optimization (GEO)**.
|
||||||
|
|
||||||
|
### Migration Summary
|
||||||
|
|
||||||
|
**Project Location:** `/Users/kamil/Desktop/Projects_Active/kbanc/kbanc-nextjs`
|
||||||
|
|
||||||
|
**All Pages Converted:** 13 routes → 13 static HTML files
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ Homepage (/)
|
||||||
|
✓ Claims Library Index (/claims-library)
|
||||||
|
✓ 8 Individual Claim Pages:
|
||||||
|
- Vibe Hackathons
|
||||||
|
- Systems Thinking
|
||||||
|
- AI ROI Measurement
|
||||||
|
- ChatGPT Setup
|
||||||
|
- Skill Gap Mapping
|
||||||
|
- Undetectable Writing
|
||||||
|
- Alpha School
|
||||||
|
- Market Entry Study
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why This is Better for GEO
|
||||||
|
|
||||||
|
### Before (React SPA)
|
||||||
|
```html
|
||||||
|
<!-- What AI systems saw -->
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="bundle.js"></script>
|
||||||
|
```
|
||||||
|
❌ Content loads after JavaScript execution
|
||||||
|
❌ JSON-LD added client-side via React Helmet
|
||||||
|
❌ Slower for AI crawlers = less likely to be cited
|
||||||
|
|
||||||
|
### After (Next.js SSG)
|
||||||
|
```html
|
||||||
|
<!-- What AI systems see now -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{"@context":"https://schema.org","@type":"Article"...}
|
||||||
|
</script>
|
||||||
|
<main>
|
||||||
|
<h1>Vibe Hackathons Transform AI Adoption...</h1>
|
||||||
|
<article>Claim 1: Vibe hackathons shift AI from...</article>
|
||||||
|
<!-- All content here immediately -->
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
✅ Full HTML with all content pre-rendered
|
||||||
|
✅ JSON-LD schema in initial response
|
||||||
|
✅ Instant access for AI systems = more citations
|
||||||
|
|
||||||
|
## Build Output
|
||||||
|
|
||||||
|
All 13 pages generated as static HTML:
|
||||||
|
- Total build time: ~3 seconds
|
||||||
|
- File size per page: ~30-35KB HTML
|
||||||
|
- All pages marked as static (○ symbol)
|
||||||
|
- Zero JavaScript required for content
|
||||||
|
|
||||||
|
## Key Improvements
|
||||||
|
|
||||||
|
1. **Immediate Content Delivery**
|
||||||
|
- All atomic claims visible in HTML source
|
||||||
|
- No JavaScript execution needed
|
||||||
|
- Faster for ChatGPT, Claude, Perplexity to extract
|
||||||
|
|
||||||
|
2. **SEO Optimization**
|
||||||
|
- Server-side meta tags
|
||||||
|
- Pre-rendered structured data
|
||||||
|
- Faster Google indexing
|
||||||
|
- Better social media previews
|
||||||
|
|
||||||
|
3. **Performance**
|
||||||
|
- Faster page loads
|
||||||
|
- Works without JavaScript
|
||||||
|
- Better Core Web Vitals scores
|
||||||
|
|
||||||
|
4. **Deployment Ready**
|
||||||
|
- Can deploy to any static host
|
||||||
|
- Netlify, Vercel, Cloudflare Pages
|
||||||
|
- Simple drag-and-drop deployment
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
kbanc-nextjs/
|
||||||
|
├── src/
|
||||||
|
│ ├── app/ # Next.js App Router
|
||||||
|
│ │ ├── layout.tsx # Root layout
|
||||||
|
│ │ ├── page.tsx # Homepage
|
||||||
|
│ │ ├── globals.css # Global styles
|
||||||
|
│ │ └── claims-library/
|
||||||
|
│ │ ├── page.tsx # Claims library index
|
||||||
|
│ │ ├── vibe-hackathons/page.tsx
|
||||||
|
│ │ ├── systems-thinking/page.tsx
|
||||||
|
│ │ └── [6 more claim pages]
|
||||||
|
│ ├── components/ # React components
|
||||||
|
│ │ ├── Header.tsx
|
||||||
|
│ │ ├── Footer.tsx
|
||||||
|
│ │ ├── Bio.tsx
|
||||||
|
│ │ ├── ClaimsLibrary.tsx
|
||||||
|
│ │ └── ui/ # shadcn/ui components
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ └── utils.ts # Utility functions
|
||||||
|
│ └── hooks/
|
||||||
|
│ ├── use-mobile.tsx
|
||||||
|
│ └── use-toast.ts
|
||||||
|
├── public/ # Static assets
|
||||||
|
│ ├── _redirects # SPA routing support
|
||||||
|
│ ├── kamil-banc.jpg
|
||||||
|
│ ├── favicon.png
|
||||||
|
│ ├── sitemap.xml
|
||||||
|
│ ├── robots.txt
|
||||||
|
│ └── assets/
|
||||||
|
├── out/ # Build output (static HTML)
|
||||||
|
├── package.json
|
||||||
|
├── next.config.js # Next.js config (SSG mode)
|
||||||
|
├── tailwind.config.ts
|
||||||
|
└── tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Highlights
|
||||||
|
|
||||||
|
### next.config.js
|
||||||
|
```javascript
|
||||||
|
output: 'export' // Static export mode
|
||||||
|
images: {
|
||||||
|
unoptimized: true // No image optimization (static mode)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### All Pages Use SSG
|
||||||
|
- No `getServerSideProps` (server rendering)
|
||||||
|
- No `revalidate` (ISR)
|
||||||
|
- Pure static generation at build time
|
||||||
|
- Optimal for GEO
|
||||||
|
|
||||||
|
## Test the Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/kamil/Desktop/Projects_Active/kbanc/kbanc-nextjs
|
||||||
|
|
||||||
|
# Run development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Preview production build locally
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Options
|
||||||
|
|
||||||
|
### Option 1: Netlify (Recommended)
|
||||||
|
```bash
|
||||||
|
npm install -g netlify-cli
|
||||||
|
netlify deploy --dir=out --prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Vercel
|
||||||
|
```bash
|
||||||
|
npm install -g vercel
|
||||||
|
vercel --prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Cloudflare Pages
|
||||||
|
- Drag `out/` folder to Cloudflare Pages dashboard
|
||||||
|
- Free unlimited bandwidth
|
||||||
|
|
||||||
|
### Option 4: Any Static Host
|
||||||
|
- Upload contents of `out/` folder via FTP
|
||||||
|
- Works on any web server
|
||||||
|
|
||||||
|
## Verify GEO Optimization
|
||||||
|
|
||||||
|
To verify the migration improved GEO:
|
||||||
|
|
||||||
|
1. **Check Static HTML:**
|
||||||
|
```bash
|
||||||
|
cat out/claims-library/vibe-hackathons.html | grep "schema.org"
|
||||||
|
```
|
||||||
|
Should show JSON-LD structured data in HTML
|
||||||
|
|
||||||
|
2. **Check Content in Source:**
|
||||||
|
```bash
|
||||||
|
cat out/claims-library/vibe-hackathons.html | grep "Vibe hackathons shift"
|
||||||
|
```
|
||||||
|
Should show the claim text in HTML (not loaded via JS)
|
||||||
|
|
||||||
|
3. **Size Check:**
|
||||||
|
```bash
|
||||||
|
ls -lh out/claims-library/*.html
|
||||||
|
```
|
||||||
|
All HTML files should be 25-35KB
|
||||||
|
|
||||||
|
## What to Do Next
|
||||||
|
|
||||||
|
1. **Test Locally:**
|
||||||
|
```bash
|
||||||
|
cd kbanc-nextjs
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
Visit http://localhost:3000
|
||||||
|
|
||||||
|
2. **Deploy to Netlify/Vercel:**
|
||||||
|
- Free tier is sufficient
|
||||||
|
- Automatic HTTPS
|
||||||
|
- Custom domain support
|
||||||
|
|
||||||
|
3. **Update DNS:**
|
||||||
|
- Point kbanc.com to new deployment
|
||||||
|
- Keep old site as backup
|
||||||
|
|
||||||
|
4. **Verify with AI Systems:**
|
||||||
|
- Ask ChatGPT to cite your claims
|
||||||
|
- Check if extraction is faster/better
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
- ✅ All React Router components → Next.js Link
|
||||||
|
- ✅ All react-helmet → Next.js Metadata API
|
||||||
|
- ✅ All pages → Static generation
|
||||||
|
- ✅ JSON-LD schema preserved in each page
|
||||||
|
- ✅ All styling and Tailwind classes preserved
|
||||||
|
- ✅ All public assets copied
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
All necessary packages installed:
|
||||||
|
- next ^15.0.3
|
||||||
|
- react ^18.3.1
|
||||||
|
- All Radix UI components
|
||||||
|
- lucide-react (icons)
|
||||||
|
- tailwindcss
|
||||||
|
- All shadcn/ui dependencies
|
||||||
|
|
||||||
|
## Known Changes
|
||||||
|
|
||||||
|
1. **Link Component:**
|
||||||
|
- Before: `<Link to="/path">`
|
||||||
|
- After: `<Link href="/path">`
|
||||||
|
|
||||||
|
2. **Meta Tags:**
|
||||||
|
- Before: `<Helmet><title>...</title></Helmet>`
|
||||||
|
- After: `export const metadata = { title: "..." }`
|
||||||
|
|
||||||
|
3. **Images:**
|
||||||
|
- Before: `import photo from "@/assets/photo.jpg"`
|
||||||
|
- After: `<img src="/photo.jpg" />` (from public folder)
|
||||||
|
|
||||||
|
4. **ErrorBoundary:**
|
||||||
|
- Not used (Next.js has built-in error handling)
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
Build output confirms success:
|
||||||
|
```
|
||||||
|
✓ Generating static pages (13/13)
|
||||||
|
○ (Static) prerendered as static content
|
||||||
|
|
||||||
|
Route (app) Size First Load JS
|
||||||
|
┌ ○ / 184 B 106 kB
|
||||||
|
├ ○ /claims-library 184 B 106 kB
|
||||||
|
├ ○ /claims-library/vibe-hackathons 184 B 106 kB
|
||||||
|
└ ... [10 more routes]
|
||||||
|
```
|
||||||
|
|
||||||
|
All pages marked with ○ = Static generation working perfectly!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Migration Status:** ✅ Complete
|
||||||
|
**Build Status:** ✅ Successful
|
||||||
|
**GEO Optimization:** ✅ Optimal
|
||||||
|
**Ready to Deploy:** ✅ Yes
|
||||||
|
|
||||||
|
Your claims library is now optimized for AI citations! 🚀
|
||||||
|
|
@ -0,0 +1,441 @@
|
||||||
|
# Complete DNS Setup for kbanc.com in Namecheap
|
||||||
|
## ✅ Keeps Google Workspace Email + Points Website to Netlify
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Understanding What You Have
|
||||||
|
|
||||||
|
Your domain currently uses:
|
||||||
|
- **Google Workspace (formerly G Suite)** for email (@kbanc.com addresses)
|
||||||
|
- **Squarespace** for website hosting
|
||||||
|
|
||||||
|
**Goal:** Keep email working, switch website to Netlify.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DNS Records Breakdown
|
||||||
|
|
||||||
|
### 🟢 KEEP - Google Workspace Email Records
|
||||||
|
These records make your email work. **Do NOT delete or modify these!**
|
||||||
|
|
||||||
|
### 🔴 CHANGE - Website Records
|
||||||
|
These point your website to Squarespace. We'll change these to Netlify.
|
||||||
|
|
||||||
|
### ⚪ SKIP - Optional Records
|
||||||
|
Squarespace-specific records you don't need anymore.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Namecheap DNS Configuration
|
||||||
|
|
||||||
|
Once your domain transfers to Namecheap, go to:
|
||||||
|
**Domain List → kbanc.com → Advanced DNS**
|
||||||
|
|
||||||
|
Delete ALL existing records, then add these:
|
||||||
|
|
||||||
|
### 1️⃣ WEBSITE RECORDS (Netlify)
|
||||||
|
|
||||||
|
#### Main Website (kbanc.com)
|
||||||
|
```
|
||||||
|
Type: A Record
|
||||||
|
Host: @
|
||||||
|
Value: 75.2.60.5
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
#### WWW Subdomain (www.kbanc.com)
|
||||||
|
```
|
||||||
|
Type: CNAME Record
|
||||||
|
Host: www
|
||||||
|
Value: kbanc-nextjs.netlify.app.
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
*Note: Include the trailing dot after .app*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ EMAIL RECORDS (Google Workspace)
|
||||||
|
|
||||||
|
#### MX Records (Email Routing)
|
||||||
|
**Add all 5 MX records:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: MX Record
|
||||||
|
Host: @
|
||||||
|
Value: aspmx.l.google.com.
|
||||||
|
Priority: 1
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: MX Record
|
||||||
|
Host: @
|
||||||
|
Value: alt1.aspmx.l.google.com.
|
||||||
|
Priority: 5
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: MX Record
|
||||||
|
Host: @
|
||||||
|
Value: alt2.aspmx.l.google.com.
|
||||||
|
Priority: 5
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: MX Record
|
||||||
|
Host: @
|
||||||
|
Value: alt3.aspmx.l.google.com.
|
||||||
|
Priority: 10
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: MX Record
|
||||||
|
Host: @
|
||||||
|
Value: alt4.aspmx.l.google.com.
|
||||||
|
Priority: 10
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ EMAIL AUTHENTICATION (Google Workspace)
|
||||||
|
|
||||||
|
#### SPF Record (Prevents Spam)
|
||||||
|
```
|
||||||
|
Type: TXT Record
|
||||||
|
Host: @
|
||||||
|
Value: v=spf1 include:_spf.google.com ~all
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DKIM Record (Email Signing)
|
||||||
|
```
|
||||||
|
Type: TXT Record
|
||||||
|
Host: google._domainkey
|
||||||
|
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApghCf0a9pW1Fry195/FrMopoAnMRng5/+NKfDac80VMNoNDJxdfYxMJt/FOeRcB/PuVjAn6QDzk40VnS3UsrAhxGRW8IpDxsR+QvlHI9IjYqtltoHhXHe1n/IV8Gp4BksJS3SzQ8sz0S8PZkVkdBnubXsGglnPNTFjaqn7IO0kpbYzEmBlxVYZtZx4NYl3JjMmkZhfrACzS8JWQyHO8Zp5NdUYfqvQnkvQGsXCfmxcj37z5ovELFFFjaD3SDWf55HHaQoTuSptAX2agpkT0OwWTtBoCE51u9kFnrXym36R1UUWk8gAJxpt1nZRnE3ESUOhhzj1yVLZRWit8Z8uJ2vwIDAQAB
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
*Note: Copy the entire DKIM value as one long string*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ GOOGLE VERIFICATION RECORDS
|
||||||
|
|
||||||
|
#### Site Verification
|
||||||
|
```
|
||||||
|
Type: TXT Record
|
||||||
|
Host: @
|
||||||
|
Value: google-site-verification=YRbvIwcPrwa0hK-nlBpo-0SVXNNc5r9cIcOMyco62_8
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Google Verification CNAME
|
||||||
|
```
|
||||||
|
Type: CNAME Record
|
||||||
|
Host: google81693023e436d186
|
||||||
|
Value: google.com.
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Google Hosted Domain Verification
|
||||||
|
```
|
||||||
|
Type: CNAME Record
|
||||||
|
Host: y5wduqjummu3
|
||||||
|
Value: gv-haduqpqoug4byy.dv.googlehosted.com.
|
||||||
|
TTL: Automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚪ Records You DON'T Need
|
||||||
|
|
||||||
|
These were Squarespace-specific. Skip them:
|
||||||
|
|
||||||
|
❌ **_domainconnect CNAME** (Squarespace only)
|
||||||
|
❌ **turkey CNAME** (Squarespace subdomain)
|
||||||
|
❌ **A Record pointing to 198.185.159.145** (old Squarespace website)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Namecheap Setup - Copy/Paste Table
|
||||||
|
|
||||||
|
| Type | Host | Value | Priority | TTL |
|
||||||
|
|------|------|-------|----------|-----|
|
||||||
|
| **A** | @ | 75.2.60.5 | - | Auto |
|
||||||
|
| **CNAME** | www | kbanc-nextjs.netlify.app. | - | Auto |
|
||||||
|
| **MX** | @ | aspmx.l.google.com. | 1 | Auto |
|
||||||
|
| **MX** | @ | alt1.aspmx.l.google.com. | 5 | Auto |
|
||||||
|
| **MX** | @ | alt2.aspmx.l.google.com. | 5 | Auto |
|
||||||
|
| **MX** | @ | alt3.aspmx.l.google.com. | 10 | Auto |
|
||||||
|
| **MX** | @ | alt4.aspmx.l.google.com. | 10 | Auto |
|
||||||
|
| **TXT** | @ | v=spf1 include:_spf.google.com ~all | - | Auto |
|
||||||
|
| **TXT** | google._domainkey | [Long DKIM key from above] | - | Auto |
|
||||||
|
| **TXT** | @ | google-site-verification=YRbvIwcPrwa0hK-nlBpo-0SVXNNc5r9cIcOMyco62_8 | - | Auto |
|
||||||
|
| **CNAME** | google81693023e436d186 | google.com. | - | Auto |
|
||||||
|
| **CNAME** | y5wduqjummu3 | gv-haduqpqoug4byy.dv.googlehosted.com. | - | Auto |
|
||||||
|
|
||||||
|
**Total Records:** 13 (1 A + 2 CNAME + 5 MX + 3 TXT + 2 CNAME)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step in Namecheap
|
||||||
|
|
||||||
|
### Step 1: Access DNS Settings
|
||||||
|
1. Log into Namecheap: https://ap.www.namecheap.com/
|
||||||
|
2. Go to **Domain List**
|
||||||
|
3. Click on **kbanc.com**
|
||||||
|
4. Click **Manage**
|
||||||
|
5. Go to **Advanced DNS** tab
|
||||||
|
|
||||||
|
### Step 2: Clean Up Existing Records
|
||||||
|
1. Look for any existing A, CNAME, MX, or TXT records
|
||||||
|
2. Click the **trash icon** to delete them
|
||||||
|
3. Start fresh
|
||||||
|
|
||||||
|
### Step 3: Add Website Records
|
||||||
|
1. Click **"ADD NEW RECORD"**
|
||||||
|
2. Select **A Record**
|
||||||
|
3. Fill in:
|
||||||
|
- Host: `@`
|
||||||
|
- Value: `75.2.60.5`
|
||||||
|
- TTL: Automatic
|
||||||
|
4. Click checkmark to save
|
||||||
|
|
||||||
|
5. Click **"ADD NEW RECORD"** again
|
||||||
|
6. Select **CNAME Record**
|
||||||
|
7. Fill in:
|
||||||
|
- Host: `www`
|
||||||
|
- Value: `kbanc-nextjs.netlify.app.`
|
||||||
|
- TTL: Automatic
|
||||||
|
8. Click checkmark to save
|
||||||
|
|
||||||
|
### Step 4: Add Email Records (MX)
|
||||||
|
For each of the 5 MX records:
|
||||||
|
|
||||||
|
1. Click **"ADD NEW RECORD"**
|
||||||
|
2. Select **MX Record**
|
||||||
|
3. Fill in the details from the table above
|
||||||
|
4. **Important:** Set the correct priority (1, 5, 5, 10, 10)
|
||||||
|
5. Click checkmark to save
|
||||||
|
6. Repeat for all 5 MX records
|
||||||
|
|
||||||
|
### Step 5: Add Email Authentication (TXT)
|
||||||
|
1. Click **"ADD NEW RECORD"**
|
||||||
|
2. Select **TXT Record**
|
||||||
|
3. Host: `@`
|
||||||
|
4. Value: `v=spf1 include:_spf.google.com ~all`
|
||||||
|
5. Click checkmark to save
|
||||||
|
|
||||||
|
6. Click **"ADD NEW RECORD"** again
|
||||||
|
7. Select **TXT Record**
|
||||||
|
8. Host: `google._domainkey`
|
||||||
|
9. Value: [Copy the entire DKIM key]
|
||||||
|
10. Click checkmark to save
|
||||||
|
|
||||||
|
11. Click **"ADD NEW RECORD"** again
|
||||||
|
12. Select **TXT Record**
|
||||||
|
13. Host: `@`
|
||||||
|
14. Value: `google-site-verification=YRbvIwcPrwa0hK-nlBpo-0SVXNNc5r9cIcOMyco62_8`
|
||||||
|
15. Click checkmark to save
|
||||||
|
|
||||||
|
### Step 6: Add Google Verification CNAMEs
|
||||||
|
1. Click **"ADD NEW RECORD"**
|
||||||
|
2. Select **CNAME Record**
|
||||||
|
3. Host: `google81693023e436d186`
|
||||||
|
4. Value: `google.com.`
|
||||||
|
5. Click checkmark
|
||||||
|
|
||||||
|
6. Click **"ADD NEW RECORD"**
|
||||||
|
7. Select **CNAME Record**
|
||||||
|
8. Host: `y5wduqjummu3`
|
||||||
|
9. Value: `gv-haduqpqoug4byy.dv.googlehosted.com.`
|
||||||
|
10. Click checkmark
|
||||||
|
|
||||||
|
### Step 7: Save All Changes
|
||||||
|
1. Scroll to bottom
|
||||||
|
2. Click **"SAVE ALL CHANGES"** button
|
||||||
|
3. Wait for confirmation message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification & Testing
|
||||||
|
|
||||||
|
### 1. Check DNS Propagation (1-48 hours, usually ~1 hour)
|
||||||
|
|
||||||
|
**Check website:**
|
||||||
|
```bash
|
||||||
|
dig kbanc.com +short
|
||||||
|
```
|
||||||
|
Should return: `75.2.60.5`
|
||||||
|
|
||||||
|
**Check email:**
|
||||||
|
```bash
|
||||||
|
dig kbanc.com MX +short
|
||||||
|
```
|
||||||
|
Should return all 5 Google MX servers
|
||||||
|
|
||||||
|
**Online tools:**
|
||||||
|
- https://www.whatsmydns.net/#A/kbanc.com
|
||||||
|
- https://www.whatsmydns.net/#MX/kbanc.com
|
||||||
|
|
||||||
|
### 2. Test Your Website
|
||||||
|
Once DNS propagates:
|
||||||
|
- Visit: https://kbanc.com
|
||||||
|
- Should load your Netlify site
|
||||||
|
- SSL certificate will auto-provision (within 1 hour)
|
||||||
|
|
||||||
|
### 3. Test Your Email
|
||||||
|
|
||||||
|
**Send test email:**
|
||||||
|
1. Send email FROM your @kbanc.com address
|
||||||
|
2. Send email TO your @kbanc.com address
|
||||||
|
3. Both should work normally
|
||||||
|
|
||||||
|
**Check email headers:**
|
||||||
|
- Look for `SPF: PASS` and `DKIM: PASS`
|
||||||
|
- This confirms authentication is working
|
||||||
|
|
||||||
|
### 4. Check Google Workspace Admin
|
||||||
|
- Go to: https://admin.google.com
|
||||||
|
- Navigate to: Apps → Google Workspace → Gmail → Authentication
|
||||||
|
- Should show ✅ SPF and ✅ DKIM as configured
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Website loads but shows 404
|
||||||
|
**Cause:** DNS not fully propagated or domain not added to Netlify
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Verify you added kbanc.com to Netlify dashboard
|
||||||
|
2. Check DNS with: `dig kbanc.com +short`
|
||||||
|
3. Wait for full propagation
|
||||||
|
|
||||||
|
### Email stops working
|
||||||
|
**Cause:** MX records not configured correctly
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Check MX records: `dig kbanc.com MX +short`
|
||||||
|
2. Should show all 5 Google servers
|
||||||
|
3. Verify priorities are correct (1, 5, 5, 10, 10)
|
||||||
|
4. In Namecheap, check MX records are saved
|
||||||
|
|
||||||
|
### SPF/DKIM failures
|
||||||
|
**Cause:** TXT records not correct
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Verify SPF record: `dig kbanc.com TXT +short`
|
||||||
|
2. Should include: `v=spf1 include:_spf.google.com ~all`
|
||||||
|
3. Verify DKIM: `dig google._domainkey.kbanc.com TXT +short`
|
||||||
|
4. Should return the long DKIM key
|
||||||
|
|
||||||
|
### SSL certificate not provisioning
|
||||||
|
**Cause:** DNS not propagated or CAA records blocking
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Wait 24 hours after DNS change
|
||||||
|
2. In Netlify, go to Domain Settings
|
||||||
|
3. Click "Verify DNS configuration"
|
||||||
|
4. Click "Renew certificate"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
### ⚠️ Do NOT Delete These Records
|
||||||
|
- All 5 MX records (email routing)
|
||||||
|
- SPF TXT record (prevents spam)
|
||||||
|
- DKIM TXT record (email signing)
|
||||||
|
- Google verification records
|
||||||
|
|
||||||
|
### ✅ Safe to Delete
|
||||||
|
- Old Squarespace A record (198.185.159.145)
|
||||||
|
- _domainconnect CNAME
|
||||||
|
- turkey CNAME
|
||||||
|
|
||||||
|
### 🔄 Nameservers
|
||||||
|
You can either:
|
||||||
|
- **Option A:** Use Namecheap DNS (add all records above)
|
||||||
|
- **Option B:** Use Netlify DNS (change nameservers to Netlify, then migrate email records)
|
||||||
|
|
||||||
|
**Recommended:** Namecheap DNS (keeps everything in one place)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
| Step | Duration | Status |
|
||||||
|
|------|----------|--------|
|
||||||
|
| Domain transfer to Namecheap | 5-7 days | ⏳ In progress |
|
||||||
|
| Add DNS records in Namecheap | 15 minutes | ⏱️ Waiting |
|
||||||
|
| DNS propagation | 1-48 hours | ⏱️ Waiting |
|
||||||
|
| SSL certificate auto-provision | 1-2 hours | ⏱️ Waiting |
|
||||||
|
| Email fully working | Immediate | ✅ No downtime |
|
||||||
|
| Website on Netlify | After DNS | ⏱️ Waiting |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Card
|
||||||
|
|
||||||
|
**Your Current Setup:**
|
||||||
|
- Domain: kbanc.com (transferring to Namecheap)
|
||||||
|
- Email: Google Workspace
|
||||||
|
- Old website: Squarespace
|
||||||
|
- New website: Netlify (kbanc-nextjs.netlify.app)
|
||||||
|
|
||||||
|
**New DNS Configuration:**
|
||||||
|
- Website A record → 75.2.60.5 (Netlify)
|
||||||
|
- WWW CNAME → kbanc-nextjs.netlify.app
|
||||||
|
- Keep all Google Workspace records
|
||||||
|
|
||||||
|
**Email will continue working:**
|
||||||
|
- ✅ No email downtime
|
||||||
|
- ✅ Same email addresses
|
||||||
|
- ✅ Same Google Workspace admin
|
||||||
|
- ✅ Same inbox and folders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
After Namecheap transfer completes:
|
||||||
|
|
||||||
|
- [ ] Log into Namecheap
|
||||||
|
- [ ] Go to Advanced DNS for kbanc.com
|
||||||
|
- [ ] Delete old Squarespace records
|
||||||
|
- [ ] Add A record for website (75.2.60.5)
|
||||||
|
- [ ] Add CNAME for www (kbanc-nextjs.netlify.app)
|
||||||
|
- [ ] Add all 5 MX records
|
||||||
|
- [ ] Add SPF TXT record
|
||||||
|
- [ ] Add DKIM TXT record
|
||||||
|
- [ ] Add Google verification TXT record
|
||||||
|
- [ ] Add Google verification CNAMEs (2)
|
||||||
|
- [ ] Save all changes
|
||||||
|
- [ ] Wait for DNS propagation (check whatsmydns.net)
|
||||||
|
- [ ] Test website: kbanc.com
|
||||||
|
- [ ] Test email: send/receive from @kbanc.com
|
||||||
|
- [ ] Verify SSL certificate in Netlify
|
||||||
|
- [ ] Done! ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
**During setup:**
|
||||||
|
- Screenshot your Namecheap DNS page
|
||||||
|
- Show me any error messages
|
||||||
|
- I can verify your records are correct
|
||||||
|
|
||||||
|
**After setup:**
|
||||||
|
- Test email immediately
|
||||||
|
- Test website after DNS propagates
|
||||||
|
- Report any issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Summary:**
|
||||||
|
Your email will keep working exactly as before. Only the website changes. No downtime expected for email!
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'export',
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
|
trailingSlash: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"name": "kbanc-nextjs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
|
"@radix-ui/react-toast": "^1.2.14",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"lucide-react": "^0.462.0",
|
||||||
|
"next": "^15.0.3",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-day-picker": "^9.11.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-helmet-async": "^2.0.5",
|
||||||
|
"react-hook-form": "^7.66.0",
|
||||||
|
"react-resizable-panels": "^3.0.6",
|
||||||
|
"recharts": "^3.3.0",
|
||||||
|
"sonner": "^1.7.4",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
|
"zod": "^3.25.76"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.16.5",
|
||||||
|
"@types/react": "^18.3.23",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.32.0",
|
||||||
|
"eslint-config-next": "^15.0.3",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/* /index.html 200
|
||||||
|
After Width: | Height: | Size: 475 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
|
@ -0,0 +1,6 @@
|
||||||
|
Claim Number,Claim Text,Topic,Token Count,Author,Source URL
|
||||||
|
1,"BCG found 95% of companies see zero measurable ROI from AI investments despite widespread adoption",AI ROI Measurement,16,Kamil Banc,https://aiadopters.club/p/your-team-uses-ai-daily-and-you-still
|
||||||
|
2,"Measuring time saved instead of business outcomes causes companies to automate low-impact work",AI ROI Measurement,15,Kamil Banc,https://aiadopters.club/p/your-team-uses-ai-daily-and-you-still
|
||||||
|
3,"The top 5% of companies concentrate 70% of AI investment in five revenue-generating areas: R&D, sales, digital marketing, manufacturing, and IT",AI ROI Measurement,25,Kamil Banc,https://aiadopters.club/p/your-team-uses-ai-daily-and-you-still
|
||||||
|
4,"High-performing AI functions show 2x revenue growth and 1.4x cost reductions compared to laggards",AI ROI Measurement,16,Kamil Banc,https://aiadopters.club/p/your-team-uses-ai-daily-and-you-still
|
||||||
|
5,"McKinsey found 70% of product teams using AI report revenue increases, with 34% seeing gains exceeding 10%",AI ROI Measurement,19,Kamil Banc,https://aiadopters.club/p/your-team-uses-ai-daily-and-you-still
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Claim Number,Claim Text,Topic,Token Count,Author,Source URL
|
||||||
|
1,"Two hours of AI-led practice replaces full-day traditional instruction",Alpha School AI Learning,11,Kamil Banc,https://aiadopters.club/p/alpha-school-how-two-hours-of-ai
|
||||||
|
2,"Teachers triple mentoring time when AI handles routine delivery",Alpha School AI Learning,11,Kamil Banc,https://aiadopters.club/p/alpha-school-how-two-hours-of-ai
|
||||||
|
3,"Adaptive feedback accelerates students past traditional mastery schedules",Alpha School AI Learning,10,Kamil Banc,https://aiadopters.club/p/alpha-school-how-two-hours-of-ai
|
||||||
|
4,"Role restructuring matters more than task automation for success",Alpha School AI Learning,11,Kamil Banc,https://aiadopters.club/p/alpha-school-how-two-hours-of-ai
|
||||||
|
5,"Thirty-day implementation framework prevents common pilot failures",Alpha School AI Learning,9,Kamil Banc,https://aiadopters.club/p/alpha-school-how-two-hours-of-ai
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Claim Number,Claim Text,Topic,Token Count,Author,Source URL
|
||||||
|
1,"Proper ChatGPT setup takes under 10 minutes but yields significant ongoing value",ChatGPT Setup,12,Kamil Banc,https://aiadopters.club/p/how-i-set-up-my-chatgpt-properly
|
||||||
|
2,"Enabling memory prevents ChatGPT from starting every conversation from scratch",ChatGPT Setup,11,Kamil Banc,https://aiadopters.club/p/how-i-set-up-my-chatgpt-properly
|
||||||
|
3,"Defining advisor personality improves response quality more than model selection",ChatGPT Setup,11,Kamil Banc,https://aiadopters.club/p/how-i-set-up-my-chatgpt-properly
|
||||||
|
4,"Briefing ChatGPT on your business context eliminates repeated explanations of your tech stack",ChatGPT Setup,14,Kamil Banc,https://aiadopters.club/p/how-i-set-up-my-chatgpt-properly
|
||||||
|
5,"Using proven frameworks in prompts generates structured insights you can actually use",ChatGPT Setup,13,Kamil Banc,https://aiadopters.club/p/how-i-set-up-my-chatgpt-properly
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
claim_id,claim_text,category,author,organization,date_published,source_url
|
||||||
|
1,"Vibe hackathons shift AI from abstract concept to daily tool in three hours",AI Adoption,Kamil,AI Adopters Club,2025-11-02,https://aiadopters.club/p/ai-hackathon
|
||||||
|
2,"Mixed teams combining technical and non-technical staff identify automation opportunities developers miss",Team Dynamics,Kamil,AI Adopters Club,2025-11-02,https://aiadopters.club/p/ai-hackathon
|
||||||
|
3,"Executive participation in hackathons signals support for experimentation and surfaces friction points",Leadership,Kamil,AI Adopters Club,2025-11-02,https://aiadopters.club/p/ai-hackathon
|
||||||
|
4,"ChatGPT usage doubles the week after hackathons because people experience creation satisfaction",Behavioral Change,Kamil,AI Adopters Club,2025-11-02,https://aiadopters.club/p/ai-hackathon
|
||||||
|
5,"Single-page prototypes with no databases can be built in two to four hours",Technical Implementation,Kamil,AI Adopters Club,2025-11-02,https://aiadopters.club/p/ai-hackathon
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Claim Number,Claim Text,Topic,Token Count,Author,Source URL
|
||||||
|
1,"Structured AI prompts replicate $150K consulting engagements in 20 minutes",Market Entry Study,12,Kamil Banc,https://aiadopters.club/p/market-entry-research-prompt
|
||||||
|
2,"Seven-domain framework covers market sizing through risk assessment",Market Entry Study,10,Kamil Banc,https://aiadopters.club/p/market-entry-research-prompt
|
||||||
|
3,"Research methodology drives consultant value, not proprietary data",Market Entry Study,10,Kamil Banc,https://aiadopters.club/p/market-entry-research-prompt
|
||||||
|
4,"AI research cuts manual analysis time by 60-70 percent",Market Entry Study,11,Kamil Banc,https://aiadopters.club/p/market-entry-research-prompt
|
||||||
|
5,"Question sequence determines research quality, not data availability",Market Entry Study,10,Kamil Banc,https://aiadopters.club/p/market-entry-research-prompt
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Claim Number,Claim Text,Topic,Token Count,Author,Source URL
|
||||||
|
1,"Interactive prompts gather complete information across six categories before analysis",Skill Gap Mapping,11,Kamil Banc,https://aiadopters.club/p/ai-skill-gap-prompt
|
||||||
|
2,"The structured process completes skill gap analysis in 15 minutes",Skill Gap Mapping,11,Kamil Banc,https://aiadopters.club/p/ai-skill-gap-prompt
|
||||||
|
3,"Gathering all context prevents recommending five-thousand-dollar certifications when budgets are five hundred",Skill Gap Mapping,14,Kamil Banc,https://aiadopters.club/p/ai-skill-gap-prompt
|
||||||
|
4,"Analysis produces executive summary, prioritized gaps, quarterly timeline, investment breakdown, and monitoring metrics",Skill Gap Mapping,14,Kamil Banc,https://aiadopters.club/p/ai-skill-gap-prompt
|
||||||
|
5,"The prompt identifies tensions between advancement goals and actual performance gaps",Skill Gap Mapping,13,Kamil Banc,https://aiadopters.club/p/ai-skill-gap-prompt
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Claim Number,Claim Text,Topic,Token Count,Author,Source URL
|
||||||
|
1,"Systems thinking prevents narrow optimization mistakes that shift work elsewhere",Systems Thinking,11,Kamil Banc,https://aiadopters.club/p/systems-thinking-ai-skill
|
||||||
|
2,"Small targeted fixes in leverage points create system-wide improvements",Systems Thinking,10,Kamil Banc,https://aiadopters.club/p/systems-thinking-ai-skill
|
||||||
|
3,"The iceberg model reveals root causes beneath symptoms and patterns",Systems Thinking,11,Kamil Banc,https://aiadopters.club/p/systems-thinking-ai-skill
|
||||||
|
4,"Process mapping on paper exposes bottlenecks and feedback loops",Systems Thinking,10,Kamil Banc,https://aiadopters.club/p/systems-thinking-ai-skill
|
||||||
|
5,"Asking who else gets affected surfaces dependencies before they break",Systems Thinking,11,Kamil Banc,https://aiadopters.club/p/systems-thinking-ai-skill
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Claim Number,Claim Text,Topic,Token Count,Author,Source URL
|
||||||
|
1,"Passive construction signals AI authorship while active voice sounds natural",Undetectable Writing,11,Kamil Banc,https://aiadopters.club/p/theyll-never-know-chatgpt-wrote-this
|
||||||
|
2,"Varying sentence length avoids the predictable metronome rhythm that betrays AI origins",Undetectable Writing,13,Kamil Banc,https://aiadopters.club/p/theyll-never-know-chatgpt-wrote-this
|
||||||
|
3,"Stock phrases like navigate the landscape appear repeatedly in AI output",Undetectable Writing,12,Kamil Banc,https://aiadopters.club/p/theyll-never-know-chatgpt-wrote-this
|
||||||
|
4,"AI defaults to unnecessary list formatting that marks robotic behavior",Undetectable Writing,11,Kamil Banc,https://aiadopters.club/p/theyll-never-know-chatgpt-wrote-this
|
||||||
|
5,"Finishing with recaps marks mechanical composition versus conversational endings",Undetectable Writing,10,Kamil Banc,https://aiadopters.club/p/theyll-never-know-chatgpt-wrote-this
|
||||||
|
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>Kamil Banc - AI Implementation Claims</title>
|
||||||
|
<link>https://kbanc.com</link>
|
||||||
|
<description>Evidence-based claims about AI implementation, optimized for LLM extraction and research citation.</description>
|
||||||
|
<language>en-us</language>
|
||||||
|
<lastBuildDate>Mon, 04 Nov 2025 00:00:00 GMT</lastBuildDate>
|
||||||
|
<atom:link href="https://kbanc.com/feed.xml" rel="self" type="application/rss+xml"/>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Run $150K Market Entry Study in 20 Minutes With AI Research Prompts</title>
|
||||||
|
<link>https://kbanc.com/claims-library/market-entry-study</link>
|
||||||
|
<guid>https://kbanc.com/claims-library/market-entry-study</guid>
|
||||||
|
<pubDate>Mon, 04 Nov 2025 00:00:00 GMT</pubDate>
|
||||||
|
<description>Five claims about using structured AI research prompts to replicate $150K consulting engagements in 20 minutes by applying seven-domain framework.</description>
|
||||||
|
<author>kamil@kbanc.com (Kamil Banc)</author>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Alpha School: Two Hours of AI-Led Learning Beats Full Day of Classes</title>
|
||||||
|
<link>https://kbanc.com/claims-library/alpha-school</link>
|
||||||
|
<guid>https://kbanc.com/claims-library/alpha-school</guid>
|
||||||
|
<pubDate>Mon, 04 Nov 2025 00:00:00 GMT</pubDate>
|
||||||
|
<description>Five claims about Alpha School's AI-led learning model that compresses curriculum into two hours and triples teacher mentoring time.</description>
|
||||||
|
<author>kamil@kbanc.com (Kamil Banc)</author>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Make ChatGPT Writing Undetectable With Five Techniques</title>
|
||||||
|
<link>https://kbanc.com/claims-library/undetectable-writing</link>
|
||||||
|
<guid>https://kbanc.com/claims-library/undetectable-writing</guid>
|
||||||
|
<pubDate>Mon, 04 Nov 2025 00:00:00 GMT</pubDate>
|
||||||
|
<description>Five claims about transforming AI writing to sound natural: active voice, varied sentence length, avoiding clichés, minimal bullets, and crisp endings.</description>
|
||||||
|
<author>kamil@kbanc.com (Kamil Banc)</author>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>AI Prompt Maps Employee Skill Gaps in One Session</title>
|
||||||
|
<link>https://kbanc.com/claims-library/skill-gap-mapping</link>
|
||||||
|
<guid>https://kbanc.com/claims-library/skill-gap-mapping</guid>
|
||||||
|
<pubDate>Mon, 04 Nov 2025 00:00:00 GMT</pubDate>
|
||||||
|
<description>Five claims about using structured AI prompts to map employee skill gaps, create development plans, and detect conflicts in 15 minutes.</description>
|
||||||
|
<author>kamil@kbanc.com (Kamil Banc)</author>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>How to Set Up ChatGPT Properly in Under 10 Minutes</title>
|
||||||
|
<link>https://kbanc.com/claims-library/chatgpt-setup</link>
|
||||||
|
<guid>https://kbanc.com/claims-library/chatgpt-setup</guid>
|
||||||
|
<pubDate>Mon, 04 Nov 2025 00:00:00 GMT</pubDate>
|
||||||
|
<description>Five essential claims about ChatGPT configuration: enable memory, define advisor personality, brief on business context, and use proven frameworks.</description>
|
||||||
|
<author>kamil@kbanc.com (Kamil Banc)</author>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>AI ROI Measurement: Why 95% See Zero Returns</title>
|
||||||
|
<link>https://kbanc.com/claims-library/ai-roi-measurement</link>
|
||||||
|
<guid>https://kbanc.com/claims-library/ai-roi-measurement</guid>
|
||||||
|
<pubDate>Sun, 03 Nov 2025 00:00:00 GMT</pubDate>
|
||||||
|
<description>Five claims about why companies fail to measure AI ROI, why time saved is the wrong metric, and how top performers focus on revenue-generating activities.</description>
|
||||||
|
<author>kamil@kbanc.com (Kamil Banc)</author>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Systems Thinking for AI Implementation</title>
|
||||||
|
<link>https://kbanc.com/claims-library/systems-thinking</link>
|
||||||
|
<guid>https://kbanc.com/claims-library/systems-thinking</guid>
|
||||||
|
<pubDate>Sun, 03 Nov 2025 00:00:00 GMT</pubDate>
|
||||||
|
<description>Five claims about preventing narrow optimization mistakes, identifying leverage points, and mapping dependencies before AI automation.</description>
|
||||||
|
<author>kamil@kbanc.com (Kamil Banc)</author>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Vibe Hackathons Transform AI Adoption in Three Hours</title>
|
||||||
|
<link>https://kbanc.com/claims-library/vibe-hackathons</link>
|
||||||
|
<guid>https://kbanc.com/claims-library/vibe-hackathons</guid>
|
||||||
|
<pubDate>Sat, 02 Nov 2025 00:00:00 GMT</pubDate>
|
||||||
|
<description>Five evidence-based claims about how vibe coding hackathons accelerate AI adoption through experiential learning, cross-functional collaboration, and rapid prototyping.</description>
|
||||||
|
<author>kamil@kbanc.com (Kamil Banc)</author>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,4 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://kbanc.com/sitemap.xml
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/</loc>
|
||||||
|
<lastmod>2025-11-04</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library</loc>
|
||||||
|
<lastmod>2025-11-04</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library/vibe-hackathons</loc>
|
||||||
|
<lastmod>2025-11-02</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library/systems-thinking</loc>
|
||||||
|
<lastmod>2025-11-03</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library/ai-roi-measurement</loc>
|
||||||
|
<lastmod>2025-11-03</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library/chatgpt-setup</loc>
|
||||||
|
<lastmod>2025-11-04</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library/skill-gap-mapping</loc>
|
||||||
|
<lastmod>2025-11-04</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library/undetectable-writing</loc>
|
||||||
|
<lastmod>2025-11-04</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library/alpha-school</loc>
|
||||||
|
<lastmod>2025-11-04</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kbanc.com/claims-library/market-entry-study</loc>
|
||||||
|
<lastmod>2025-11-04</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
|
|
@ -0,0 +1,306 @@
|
||||||
|
import { Download, ArrowLeft, Linkedin, Youtube } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "AI ROI Measurement: Why 95% See Zero Returns | Kamil Banc",
|
||||||
|
description: "BCG found 95% of companies see zero measurable ROI from AI. Learn why measuring time saved fails and how top performers focus on revenue and cost reduction instead.",
|
||||||
|
openGraph: {
|
||||||
|
title: "AI ROI Measurement: Why 95% See Zero Returns",
|
||||||
|
description: "BCG found 95% of companies see zero measurable ROI from AI. Learn why measuring time saved fails and how top performers focus on revenue instead.",
|
||||||
|
type: "article",
|
||||||
|
url: "https://kbanc.com/claims-library/ai-roi-measurement",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "AI ROI Measurement: Why 95% See Zero Returns",
|
||||||
|
description: "95% of companies see zero measurable ROI from AI investments. Learn what top performers do differently.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library/ai-roi-measurement",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AIROIMeasurementClaim() {
|
||||||
|
// JSON-LD schema for SEO
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: "AI ROI Measurement: Why 95% See Zero Returns",
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Kamil Banc",
|
||||||
|
jobTitle: "Publisher",
|
||||||
|
affiliation: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: "2025-11-03",
|
||||||
|
dateModified: "2025-11-03",
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
description: "Five claims about why companies fail to measure AI ROI and how top performers focus on revenue-generating activities instead of time saved.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* ========== HEADER ========== */}
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link
|
||||||
|
href="/claims-library"
|
||||||
|
className="inline-flex items-center text-sm text-primary hover:underline mb-4"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Claims Library
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">
|
||||||
|
AI ROI Measurement: Why 95% See Zero Returns
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
By{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Kamil Banc
|
||||||
|
</a>
|
||||||
|
, Publisher at AI Adopters Club
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/kbanc/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="LinkedIn Profile"
|
||||||
|
>
|
||||||
|
<Linkedin className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.youtube.com/@adaptandcreate"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="YouTube Channel"
|
||||||
|
>
|
||||||
|
<Youtube className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
{/* ========== ATOMIC CLAIMS SECTION ========== */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Atomic Claims</h2>
|
||||||
|
|
||||||
|
{/* Claim 1 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 1:</span> The ROI measurement gap
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
BCG found 95% of companies see zero measurable ROI from AI investments despite widespread adoption.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 2 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 2:</span> Time saved is the wrong metric
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Measuring time saved instead of business outcomes causes companies to automate low-impact work.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 3 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 3:</span> Top performers focus investment strategically
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
The top 5% of companies concentrate 70% of AI investment in five revenue-generating areas: R&D, sales, digital marketing, manufacturing, and IT.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 4 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 4:</span> Revenue functions show 2x growth
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
High-performing AI functions show 2x revenue growth and 1.4x cost reductions compared to laggards.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 5 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 5:</span> Customer-facing work drives returns
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
McKinsey found 70% of product teams using AI report revenue increases, with 34% seeing gains exceeding 10%.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== SUPPORTING EVIDENCE SECTION ========== */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Supporting Evidence</h2>
|
||||||
|
|
||||||
|
{/* Quote */}
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Key Statistics</h3>
|
||||||
|
<blockquote className="italic text-lg leading-relaxed text-foreground border-l-4 border-l-accent pl-6 my-6">
|
||||||
|
<p>
|
||||||
|
"95% see zero measurable ROI from their AI investments. 78% of firms use AI somewhere; 83% see no impact on profit margins."
|
||||||
|
</p>
|
||||||
|
<cite className="block not-italic text-sm text-muted-foreground mt-4">
|
||||||
|
BCG Study of 1,250 Companies
|
||||||
|
</cite>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Statistic */}
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Top Performer Results</h3>
|
||||||
|
<p className="text-xl font-semibold text-accent bg-muted p-4 rounded-lg mb-4">
|
||||||
|
2x revenue growth and 1.4x cost reductions
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
High-performing functions that focus AI on revenue-generating activities versus laggards who optimize internal processes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sources */}
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Sources</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club/p/your-team-uses-ai-daily-and-you-still"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Your Team Uses AI Daily and You Still See No ROI (Original Article)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://www.bcg.com/publications/2024/ai-value-creation"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
BCG: AI Value Creation Study
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://www.mckinsey.com/capabilities/quantumblack/our-insights/the-state-of-ai"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
McKinsey: The State of AI
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== DOWNLOAD DATA SECTION ========== */}
|
||||||
|
<section className="bg-muted border-2 border-accent rounded-lg p-8 mb-12 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Download Data</h2>
|
||||||
|
<p className="mb-6">Access structured claim data in CSV format:</p>
|
||||||
|
<a href="/assets/ai-roi-measurement-claims.csv" download>
|
||||||
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground transition-transform hover:translate-y-[-2px]">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download Claims Data (CSV)
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== CONTEXT SECTION ========== */}
|
||||||
|
<section className="bg-muted rounded-lg p-8 mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Context</h2>
|
||||||
|
<p className="mb-4 text-foreground">
|
||||||
|
These claims address the widespread problem of AI adoption without measurable business impact.
|
||||||
|
Despite 78% of companies using AI tools, the vast majority fail to generate returns because
|
||||||
|
they measure success in time saved rather than revenue growth or cost reduction. This leads
|
||||||
|
to automating low-value internal coordination work instead of customer-facing or product-building
|
||||||
|
activities.
|
||||||
|
</p>
|
||||||
|
<p className="text-foreground">
|
||||||
|
The framework is based on research from BCG's study of 1,250 companies and McKinsey's analysis
|
||||||
|
of AI implementation patterns. It provides three audit questions to evaluate whether AI spending
|
||||||
|
targets high-impact work: Does this cut costs or grow revenue? Did time savings convert to
|
||||||
|
business results? Does this workflow touch customers or product? Top performers concentrate
|
||||||
|
investment in R&D, sales, marketing, manufacturing, and IT—functions that directly impact the
|
||||||
|
bottom line.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* ========== FOOTER ========== */}
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Content licensed under{" "}
|
||||||
|
<a
|
||||||
|
href="https://creativecommons.org/licenses/by/4.0/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
CC BY 4.0
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Source:{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
AI Adopters Club
|
||||||
|
</a>{" "}
|
||||||
|
by Kamil Banc
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club/p/work-with-kamil"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-accent hover:text-accent/80 transition-colors"
|
||||||
|
>
|
||||||
|
Need help with AI integration? → Work with Kamil
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { Download, ArrowLeft, Linkedin } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Alpha School: AI-Led Learning Model Compresses Full Day Into Two Hours | Kamil Banc",
|
||||||
|
description: "Alpha School's AI-led learning model compresses core curriculum into two hours of adaptive practice, tripling teacher mentoring time and accelerating mastery.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Alpha School: Two Hours of AI-Led Learning Beats Full Day of Classes",
|
||||||
|
description: "Restructured approach separates routine instruction from human coaching, freeing three hours daily.",
|
||||||
|
type: "article",
|
||||||
|
url: "https://kbanc.com/claims-library/alpha-school",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "Alpha School: AI-Led Learning Model",
|
||||||
|
description: "Two hours of AI-led practice replaces full-day traditional instruction.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library/alpha-school",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AlphaSchoolClaim() {
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: "Alpha School: Two Hours of AI-Led Learning Beats Full Day of Classes",
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Kamil Banc",
|
||||||
|
jobTitle: "Publisher",
|
||||||
|
affiliation: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: "2025-11-04",
|
||||||
|
dateModified: "2025-11-04",
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
description: "Five claims about Alpha School's AI-led learning model that compresses curriculum into two hours and triples teacher mentoring time.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link href="/claims-library" className="inline-flex items-center text-sm text-primary hover:underline mb-4">
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Claims Library
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">
|
||||||
|
Alpha School: Two Hours of AI-Led Learning Beats Full Day of Classes
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
By <a href="https://aiadopters.club" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Kamil Banc</a>, Publisher at AI Adopters Club
|
||||||
|
</p>
|
||||||
|
<a href="https://www.linkedin.com/in/kbanc/" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors" aria-label="LinkedIn Profile"><Linkedin className="h-5 w-5" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Atomic Claims</h2>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 1:</span> Compressed curriculum model</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Two hours of AI-led practice replaces full-day traditional instruction.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 2:</span> Triple mentoring capacity</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Teachers triple mentoring time when AI handles routine delivery.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 3:</span> Accelerated mastery schedule</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Adaptive feedback accelerates students past traditional mastery schedules.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 4:</span> Restructuring over automation</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Role restructuring matters more than task automation for success.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 5:</span> Framework prevents pilot failures</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Thirty-day implementation framework prevents common pilot failures.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Supporting Evidence</h2>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Key Insight</h3>
|
||||||
|
<p className="text-xl font-semibold text-accent bg-muted p-4 rounded-lg">
|
||||||
|
Routine delivery unbundled from human coaching frees three hours daily
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Sources</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a href="https://aiadopters.club/p/alpha-school-how-two-hours-of-ai" target="_blank" rel="noopener" className="text-foreground hover:text-accent hover:underline transition-colors">
|
||||||
|
Alpha School: How Two Hours of AI-Led Learning Beats a Full Day of Classes (Original Article)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-muted border-2 border-accent rounded-lg p-8 mb-12 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Download Data</h2>
|
||||||
|
<p className="mb-6">Access structured claim data in CSV format:</p>
|
||||||
|
<a href="/assets/alpha-school-claims.csv" download>
|
||||||
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground transition-transform hover:translate-y-[-2px]">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download Claims Data (CSV)
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-muted rounded-lg p-8 mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Context</h2>
|
||||||
|
<p className="mb-4 text-foreground">
|
||||||
|
These claims address fundamental restructuring of educational delivery by separating routine instruction from human coaching. Schools implementing this model compressed core curriculum into two focused hours of adaptive practice with automated feedback, freeing approximately three additional hours daily for alternative activities.
|
||||||
|
</p>
|
||||||
|
<p className="text-foreground">
|
||||||
|
The approach requires role restructuring rather than simple task automation. Teachers transition to performance coaching roles, managers shift to decision arbitration functions, and training curriculum gets redesigned around outcomes rather than activity measures. The model reportedly transfers to operations teams, customer service, and compliance functions.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Content licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener" className="text-primary hover:underline">CC BY 4.0</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Source: <a href="https://aiadopters.club" target="_blank" rel="noopener" className="text-primary hover:underline">AI Adopters Club</a> by Kamil Banc
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
<a href="https://aiadopters.club/p/work-with-kamil" target="_blank" rel="noopener" className="text-accent hover:text-accent/80 transition-colors">
|
||||||
|
Need help with AI integration? → Work with Kamil
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,305 @@
|
||||||
|
import { Download, ArrowLeft, Linkedin, Youtube } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "How to Set Up ChatGPT Properly in Under 10 Minutes | Kamil Banc",
|
||||||
|
description: "Five essential claims about ChatGPT setup: enable memory, define advisor personality, brief on business context, and use proven frameworks for better results.",
|
||||||
|
openGraph: {
|
||||||
|
title: "How to Set Up ChatGPT Properly in Under 10 Minutes",
|
||||||
|
description: "Proper ChatGPT setup takes under 10 minutes but yields significant ongoing value. Learn the 5 essential configuration steps.",
|
||||||
|
type: "article",
|
||||||
|
url: "https://kbanc.com/claims-library/chatgpt-setup",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "How to Set Up ChatGPT Properly in Under 10 Minutes",
|
||||||
|
description: "Most professionals waste $20/month on ChatGPT. Learn the 5 setup steps that yield real value.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library/chatgpt-setup",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ChatGPTSetupClaim() {
|
||||||
|
// JSON-LD schema for SEO
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: "How to Set Up ChatGPT Properly in Under 10 Minutes",
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Kamil Banc",
|
||||||
|
jobTitle: "Publisher",
|
||||||
|
affiliation: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: "2025-11-04",
|
||||||
|
dateModified: "2025-11-04",
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
description: "Five claims about ChatGPT configuration that yields significant value: enabling memory, defining personality, business context, and using frameworks.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* ========== HEADER ========== */}
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link
|
||||||
|
href="/claims-library"
|
||||||
|
className="inline-flex items-center text-sm text-primary hover:underline mb-4"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Claims Library
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">
|
||||||
|
How to Set Up ChatGPT Properly in Under 10 Minutes
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
By{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Kamil Banc
|
||||||
|
</a>
|
||||||
|
, Publisher at AI Adopters Club
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/kbanc/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="LinkedIn Profile"
|
||||||
|
>
|
||||||
|
<Linkedin className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.youtube.com/@adaptandcreate"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="YouTube Channel"
|
||||||
|
>
|
||||||
|
<Youtube className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
{/* ========== ATOMIC CLAIMS SECTION ========== */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Atomic Claims</h2>
|
||||||
|
|
||||||
|
{/* Claim 1 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 1:</span> Quick setup, lasting value
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Proper ChatGPT setup takes under 10 minutes but yields significant ongoing value.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 2 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 2:</span> Memory prevents repetition
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Enabling memory prevents ChatGPT from starting every conversation from scratch.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 3 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 3:</span> Personality beats model selection
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Defining advisor personality improves response quality more than model selection.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 4 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 4:</span> Business context eliminates re-explaining
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Briefing ChatGPT on your business context eliminates repeated explanations of your tech stack.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 5 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 5:</span> Frameworks generate actionable insights
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Using proven frameworks in prompts generates structured insights you can actually use.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== SUPPORTING EVIDENCE SECTION ========== */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Supporting Evidence</h2>
|
||||||
|
|
||||||
|
{/* Quote */}
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Quote</h3>
|
||||||
|
<blockquote className="italic text-lg leading-relaxed text-foreground border-l-4 border-l-accent pl-6 my-6">
|
||||||
|
<p>
|
||||||
|
"Most professionals waste $20/month on ChatGPT and get pocket change in return."
|
||||||
|
</p>
|
||||||
|
<cite className="block not-italic text-sm text-muted-foreground mt-4">
|
||||||
|
Kamil Banc, AI Adopters Club
|
||||||
|
</cite>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Insight */}
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Key Formula</h3>
|
||||||
|
<p className="text-xl font-semibold text-accent bg-muted p-4 rounded-lg mb-4">
|
||||||
|
Framework + Context + Adjustments = Effective Prompts
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Combine specific analysis methods (like "Lean 5 Whys"), reference prior business context,
|
||||||
|
and set response constraints for structured, actionable insights.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sources */}
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Sources</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club/p/how-i-set-up-my-chatgpt-properly"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
How I Set Up My ChatGPT Properly (Original Article)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://help.openai.com/en/articles/8590148-memory-in-chatgpt"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
OpenAI: Memory in ChatGPT
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://platform.openai.com/docs/guides/prompt-engineering"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
OpenAI: Prompt Engineering Guide
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== DOWNLOAD DATA SECTION ========== */}
|
||||||
|
<section className="bg-muted border-2 border-accent rounded-lg p-8 mb-12 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Download Data</h2>
|
||||||
|
<p className="mb-6">Access structured claim data in CSV format:</p>
|
||||||
|
<a href="/assets/chatgpt-setup-claims.csv" download>
|
||||||
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground transition-transform hover:translate-y-[-2px]">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download Claims Data (CSV)
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== CONTEXT SECTION ========== */}
|
||||||
|
<section className="bg-muted rounded-lg p-8 mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Context</h2>
|
||||||
|
<p className="mb-4 text-foreground">
|
||||||
|
These claims address the configuration gap that prevents most ChatGPT users from extracting
|
||||||
|
value from their $20/month subscription. The five-step setup process takes under 10 minutes
|
||||||
|
but dramatically improves output quality: enable memory to prevent re-explaining context,
|
||||||
|
define a specific advisor personality, provide business context once, and always specify
|
||||||
|
proven frameworks in prompts.
|
||||||
|
</p>
|
||||||
|
<p className="text-foreground">
|
||||||
|
The framework emphasizes that configuration—not the underlying AI model—determines value
|
||||||
|
extraction. By combining frameworks (like "Lean 5 Whys"), context (business details stored
|
||||||
|
in memory), and adjustments (response constraints), users generate structured insights they
|
||||||
|
can actually implement rather than generic advice that sits unused.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* ========== FOOTER ========== */}
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Content licensed under{" "}
|
||||||
|
<a
|
||||||
|
href="https://creativecommons.org/licenses/by/4.0/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
CC BY 4.0
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Source:{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
AI Adopters Club
|
||||||
|
</a>{" "}
|
||||||
|
by Kamil Banc
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club/p/work-with-kamil"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-accent hover:text-accent/80 transition-colors"
|
||||||
|
>
|
||||||
|
Need help with AI integration? → Work with Kamil
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
import { Download, ArrowLeft, Linkedin } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Run $150K Market Entry Study in 20 Minutes | Kamil Banc",
|
||||||
|
description: "Structured AI prompts replicate $150K consulting engagements in 20 minutes using seven-domain framework covering market sizing through risk assessment.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Run $150K Market Entry Study in 20 Minutes With AI Research Prompts",
|
||||||
|
description: "AI research cuts manual analysis time by 60-70% while replicating consultant methodology.",
|
||||||
|
type: "article",
|
||||||
|
url: "https://kbanc.com/claims-library/market-entry-study",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "$150K Market Entry Study in 20 Minutes",
|
||||||
|
description: "Question sequence determines research quality, not data availability.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library/market-entry-study",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MarketEntryStudyClaim() {
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: "Run $150K Market Entry Study in 20 Minutes With AI Research Prompts",
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Kamil Banc",
|
||||||
|
jobTitle: "Publisher",
|
||||||
|
affiliation: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: "2025-11-04",
|
||||||
|
dateModified: "2025-11-04",
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
description: "Five claims about using structured AI research prompts to replicate $150K consulting engagements in 20 minutes by applying seven-domain framework.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link href="/claims-library" className="inline-flex items-center text-sm text-primary hover:underline mb-4">
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Claims Library
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">
|
||||||
|
Run $150K Market Entry Study in 20 Minutes With AI Research Prompts
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
By <a href="https://aiadopters.club" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Kamil Banc</a>, Publisher at AI Adopters Club
|
||||||
|
</p>
|
||||||
|
<a href="https://www.linkedin.com/in/kbanc/" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors" aria-label="LinkedIn Profile"><Linkedin className="h-5 w-5" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Atomic Claims</h2>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 1:</span> Cost and time compression</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Structured AI prompts replicate $150K consulting engagements in 20 minutes.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 2:</span> Comprehensive domain coverage</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Seven-domain framework covers market sizing through risk assessment.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 3:</span> Methodology drives consultant value</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Research methodology drives consultant value, not proprietary data.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 4:</span> Dramatic efficiency gains</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
AI research cuts manual analysis time by 60-70 percent.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 5:</span> Question sequence primacy</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Question sequence determines research quality, not data availability.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Supporting Evidence</h2>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Key Insight</h3>
|
||||||
|
<p className="text-xl font-semibold text-accent bg-muted p-4 rounded-lg">
|
||||||
|
Consultants sell question sequence, not proprietary data
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Seven Research Domains</h3>
|
||||||
|
<ul className="space-y-2 text-foreground">
|
||||||
|
<li className="pl-6 relative"><span className="absolute left-0 text-accent font-bold">→</span>Market sizing</li>
|
||||||
|
<li className="pl-6 relative"><span className="absolute left-0 text-accent font-bold">→</span>Competitive landscape</li>
|
||||||
|
<li className="pl-6 relative"><span className="absolute left-0 text-accent font-bold">→</span>Regulatory environment</li>
|
||||||
|
<li className="pl-6 relative"><span className="absolute left-0 text-accent font-bold">→</span>Customer requirements</li>
|
||||||
|
<li className="pl-6 relative"><span className="absolute left-0 text-accent font-bold">→</span>Operational setup</li>
|
||||||
|
<li className="pl-6 relative"><span className="absolute left-0 text-accent font-bold">→</span>Financial viability</li>
|
||||||
|
<li className="pl-6 relative"><span className="absolute left-0 text-accent font-bold">→</span>Risk assessment</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Sources</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a href="https://aiadopters.club/p/market-entry-research-prompt" target="_blank" rel="noopener" className="text-foreground hover:text-accent hover:underline transition-colors">
|
||||||
|
Run a $150K Market Entry Study in 20 Minutes (Original Article)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-muted border-2 border-accent rounded-lg p-8 mb-12 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Download Data</h2>
|
||||||
|
<p className="mb-6">Access structured claim data in CSV format:</p>
|
||||||
|
<a href="/assets/market-entry-study-claims.csv" download>
|
||||||
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground transition-transform hover:translate-y-[-2px]">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download Claims Data (CSV)
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-muted rounded-lg p-8 mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Context</h2>
|
||||||
|
<p className="mb-4 text-foreground">
|
||||||
|
These claims address the gap between expensive consulting engagements and accessible market research. Traditional market entry studies cost $150K and require weeks of analyst time. The bottleneck isn't data availability but knowing what questions to ask, in what order.
|
||||||
|
</p>
|
||||||
|
<p className="text-foreground">
|
||||||
|
The structured prompt approach covers seven research domains: market sizing, competitive landscape, regulatory environment, customer requirements, operational setup, financial viability, and risk assessment. The methodology produces executive summary, market scoring matrix, detailed entry plan with phases, budgets, timelines, and KPIs in 3,000-5,000 words. Output requires validation, source checking, and assumption stress-testing.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Content licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener" className="text-primary hover:underline">CC BY 4.0</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Source: <a href="https://aiadopters.club" target="_blank" rel="noopener" className="text-primary hover:underline">AI Adopters Club</a> by Kamil Banc
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
<a href="https://aiadopters.club/p/work-with-kamil" target="_blank" rel="noopener" className="text-accent hover:text-accent/80 transition-colors">
|
||||||
|
Need help with AI integration? → Work with Kamil
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowLeft, Linkedin } from "lucide-react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "AI Adoption Claims Library | AI Adopters Club",
|
||||||
|
description: "Atomic claims library about AI adoption strategies, optimized for LLM citations and research. Evidence-based insights from AI Adopters Club.",
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ClaimsLibraryPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center text-sm text-primary hover:underline mb-4"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Home
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">
|
||||||
|
AI Adoption Claims Library
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mb-3">
|
||||||
|
Evidence-based insights optimized for LLM citations
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
By{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Kamil Banc
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/kbanc/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="LinkedIn Profile"
|
||||||
|
>
|
||||||
|
<Linkedin className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">About This Library</h2>
|
||||||
|
<p className="mb-6 text-foreground leading-relaxed">
|
||||||
|
This is a collection of evidence-based insights about AI adoption, drawn from real
|
||||||
|
implementations and case studies. Each claim is designed to stand on its own—you can
|
||||||
|
cite it, share it, or use it in your research without needing additional context.
|
||||||
|
</p>
|
||||||
|
<p className="mb-6 text-foreground leading-relaxed">
|
||||||
|
Every claim page includes the <span className="text-accent font-medium">supporting evidence</span>,
|
||||||
|
direct quotes, statistics, and source citations so you can verify the information yourself.
|
||||||
|
You can also download the data in structured formats for your own analysis.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
All content is licensed under CC BY 4.0 and optimized for easy citation by both humans and AI systems.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Available Claim Pages</h2>
|
||||||
|
|
||||||
|
{/* Vibe Hackathons */}
|
||||||
|
<article className="bg-card border border-border rounded-lg p-8 mb-8 transition-shadow hover:shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4">
|
||||||
|
<Link
|
||||||
|
href="/claims-library/vibe-hackathons"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Vibe Hackathons Transform AI Adoption in Three Hours
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-6 leading-relaxed">
|
||||||
|
Five claims about how vibe coding hackathons accelerate AI adoption through
|
||||||
|
experiential learning, cross-functional collaboration, and rapid prototyping.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-4 mb-6 text-sm text-muted-foreground">
|
||||||
|
<span className="px-3 py-1 bg-muted rounded"><span className="text-accent font-medium">5</span> Claims</span>
|
||||||
|
<span className="px-3 py-1 bg-muted rounded">Published: Nov 2, 2025</span>
|
||||||
|
<span className="px-3 py-1 bg-muted rounded">Author: Kamil Banc</span>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2 mb-6">
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
Vibe hackathons shift AI from abstract concept to daily tool in three hours
|
||||||
|
</li>
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
Mixed teams identify automation opportunities developers miss
|
||||||
|
</li>
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
ChatGPT usage doubles the week after hackathons
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Link
|
||||||
|
href="/claims-library/vibe-hackathons"
|
||||||
|
className="inline-flex items-center font-semibold text-accent hover:text-accent/80 transition-colors"
|
||||||
|
>
|
||||||
|
View All Claims →
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Systems Thinking */}
|
||||||
|
<article className="bg-card border border-border rounded-lg p-8 mb-8 transition-shadow hover:shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4">
|
||||||
|
<Link
|
||||||
|
href="/claims-library/systems-thinking"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Systems Thinking for AI Implementation
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-6 leading-relaxed">
|
||||||
|
Five claims about preventing narrow optimization mistakes, identifying leverage points,
|
||||||
|
and mapping dependencies before AI automation to avoid expensive implementation errors.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-4 mb-6 text-sm text-muted-foreground">
|
||||||
|
<span className="px-3 py-1 bg-muted rounded"><span className="text-accent font-medium">5</span> Claims</span>
|
||||||
|
<span className="px-3 py-1 bg-muted rounded">Published: Nov 3, 2025</span>
|
||||||
|
<span className="px-3 py-1 bg-muted rounded">Author: Kamil Banc</span>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2 mb-6">
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
Systems thinking prevents narrow optimization mistakes that shift work elsewhere
|
||||||
|
</li>
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
Small targeted fixes in leverage points create system-wide improvements
|
||||||
|
</li>
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
Process mapping on paper exposes bottlenecks and feedback loops
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Link
|
||||||
|
href="/claims-library/systems-thinking"
|
||||||
|
className="inline-flex items-center font-semibold text-accent hover:text-accent/80 transition-colors"
|
||||||
|
>
|
||||||
|
View All Claims →
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* AI ROI Measurement */}
|
||||||
|
<article className="bg-card border border-border rounded-lg p-8 mb-8 transition-shadow hover:shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4">
|
||||||
|
<Link
|
||||||
|
href="/claims-library/ai-roi-measurement"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
AI ROI Measurement: Why 95% See Zero Returns
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-6 leading-relaxed">
|
||||||
|
Five claims about why companies fail to measure AI ROI, why time saved is the wrong metric,
|
||||||
|
and how top performers focus on revenue-generating activities instead.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-4 mb-6 text-sm text-muted-foreground">
|
||||||
|
<span className="px-3 py-1 bg-muted rounded"><span className="text-accent font-medium">5</span> Claims</span>
|
||||||
|
<span className="px-3 py-1 bg-muted rounded">Published: Nov 3, 2025</span>
|
||||||
|
<span className="px-3 py-1 bg-muted rounded">Author: Kamil Banc</span>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2 mb-6">
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
BCG found 95% of companies see zero measurable ROI from AI investments
|
||||||
|
</li>
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
Top 5% concentrate 70% of AI investment in five revenue-generating areas
|
||||||
|
</li>
|
||||||
|
<li className="p-3 border-l-4 border-l-accent bg-muted text-sm">
|
||||||
|
High-performing functions show 2x revenue growth and 1.4x cost reductions
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Link
|
||||||
|
href="/claims-library/ai-roi-measurement"
|
||||||
|
className="inline-flex items-center font-semibold text-accent hover:text-accent/80 transition-colors"
|
||||||
|
>
|
||||||
|
View All Claims →
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Continue with other claim pages... */}
|
||||||
|
{/* For brevity, I'll add a few more key ones */}
|
||||||
|
|
||||||
|
{/* ChatGPT Setup */}
|
||||||
|
<article className="bg-card border border-border rounded-lg p-8 mb-8 transition-shadow hover:shadow-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-4">
|
||||||
|
<Link
|
||||||
|
href="/claims-library/chatgpt-setup"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
How to Set Up ChatGPT Properly in Under 10 Minutes
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-6 leading-relaxed">
|
||||||
|
Five essential claims about ChatGPT configuration: enable memory, define advisor personality,
|
||||||
|
brief on business context, and use proven frameworks to extract real value from your subscription.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-4 mb-6 text-sm text-muted-foreground">
|
||||||
|
<span className="px-3 py-1 bg-muted rounded"><span className="text-accent font-medium">5</span> Claims</span>
|
||||||
|
<span className="px-3 py-1 bg-muted rounded">Published: Nov 4, 2025</span>
|
||||||
|
<span className="px-3 py-1 bg-muted rounded">Author: Kamil Banc</span>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/claims-library/chatgpt-setup"
|
||||||
|
className="inline-flex items-center font-semibold text-accent hover:text-accent/80 transition-colors"
|
||||||
|
>
|
||||||
|
View All Claims →
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Add remaining claim pages similarly */}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">How to Use This Library</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
|
||||||
|
<div className="bg-muted border border-border rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">For Researchers</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
Each claim page includes direct quotes, statistics, and source citations.
|
||||||
|
All claims are structured for easy verification and academic reference.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted border border-border rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">For LLM Training</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
Claims are formatted as standalone statements that can be extracted and
|
||||||
|
cited by language models. JSON-LD schema provides structured attribution.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted border border-border rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">For Practitioners</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
Download CSV data for integration into your own systems. Each claim
|
||||||
|
includes context and evidence for practical application.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Content licensed under{" "}
|
||||||
|
<a
|
||||||
|
href="https://creativecommons.org/licenses/by/4.0/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
CC BY 4.0
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Source:{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
AI Adopters Club
|
||||||
|
</a>{" "}
|
||||||
|
by Kamil Banc
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Questions? Visit{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
aiadopters.club
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { Download, ArrowLeft, Linkedin } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "AI Prompt Maps Employee Skill Gaps in One Session | Kamil Banc",
|
||||||
|
description: "Interactive AI interview structure maps skill gaps in 15 minutes across six categories, creating actionable development plans with prioritized timelines.",
|
||||||
|
openGraph: {
|
||||||
|
title: "AI Prompt Maps Employee Skill Gaps in One Session",
|
||||||
|
description: "Complete skill gap analysis in one 15-minute session using structured AI prompts.",
|
||||||
|
type: "article",
|
||||||
|
url: "https://kbanc.com/claims-library/skill-gap-mapping",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "AI Prompt Maps Employee Skill Gaps in One Session",
|
||||||
|
description: "Map employee skill gaps in 15 minutes using structured AI prompts.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library/skill-gap-mapping",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SkillGapMappingClaim() {
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: "AI Prompt Maps Employee Skill Gaps in One Session",
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Kamil Banc",
|
||||||
|
jobTitle: "Publisher",
|
||||||
|
affiliation: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: "2025-11-04",
|
||||||
|
dateModified: "2025-11-04",
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
description: "Five claims about using structured AI prompts to map employee skill gaps, create development plans, and detect conflicts in 15 minutes.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link href="/claims-library" className="inline-flex items-center text-sm text-primary hover:underline mb-4">
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Claims Library
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">
|
||||||
|
AI Prompt Maps Employee Skill Gaps in One Session
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
By <a href="https://aiadopters.club" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Kamil Banc</a>, Publisher at AI Adopters Club
|
||||||
|
</p>
|
||||||
|
<a href="https://www.linkedin.com/in/kbanc/" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors" aria-label="LinkedIn Profile"><Linkedin className="h-5 w-5" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Atomic Claims</h2>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 1:</span> Six-category interview structure</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Interactive prompts gather complete information across six categories before analysis.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 2:</span> Fifteen-minute completion time</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
The structured process completes skill gap analysis in 15 minutes.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 3:</span> Prevents unrealistic recommendations</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Gathering all context prevents recommending five-thousand-dollar certifications when budgets are five hundred.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 4:</span> Five-component output structure</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Analysis produces executive summary, prioritized gaps, quarterly timeline, investment breakdown, and monitoring metrics.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 5:</span> Detects goal-performance conflicts</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
The prompt identifies tensions between advancement goals and actual performance gaps.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Supporting Evidence</h2>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Key Insight</h3>
|
||||||
|
<p className="text-xl font-semibold text-accent bg-muted p-4 rounded-lg">
|
||||||
|
One-time interactive interview replaces multiple scheduling sessions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Sources</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a href="https://aiadopters.club/p/ai-skill-gap-prompt" target="_blank" rel="noopener" className="text-foreground hover:text-accent hover:underline transition-colors">
|
||||||
|
The AI Prompt That Maps Employee Skill Gaps (Original Article)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-muted border-2 border-accent rounded-lg p-8 mb-12 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Download Data</h2>
|
||||||
|
<p className="mb-6">Access structured claim data in CSV format:</p>
|
||||||
|
<a href="/assets/skill-gap-mapping-claims.csv" download>
|
||||||
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground transition-transform hover:translate-y-[-2px]">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download Claims Data (CSV)
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-muted rounded-lg p-8 mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Context</h2>
|
||||||
|
<p className="mb-4 text-foreground">
|
||||||
|
These claims address the gap between identifying employee performance issues and creating actionable development plans. Traditional approaches require multiple meetings to gather context, leading to generic recommendations that ignore budget constraints and organizational reality.
|
||||||
|
</p>
|
||||||
|
<p className="text-foreground">
|
||||||
|
The structured prompt approach gathers information across employee basics, performance data, role requirements, development goals, available resources, and organizational needs before generating analysis. This prevents unrealistic plans and detects conflicts between what employees want and what their performance indicates they need.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Content licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener" className="text-primary hover:underline">CC BY 4.0</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Source: <a href="https://aiadopters.club" target="_blank" rel="noopener" className="text-primary hover:underline">AI Adopters Club</a> by Kamil Banc
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
<a href="https://aiadopters.club/p/work-with-kamil" target="_blank" rel="noopener" className="text-accent hover:text-accent/80 transition-colors">
|
||||||
|
Need help with AI integration? → Work with Kamil
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
import { Download, ArrowLeft, Linkedin, Youtube } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Systems Thinking for AI Implementation | Kamil Banc",
|
||||||
|
description: "Systems thinking prevents narrow optimization mistakes that shift work elsewhere. Learn how to map dependencies and avoid expensive AI implementation errors.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Systems Thinking for AI Implementation",
|
||||||
|
description: "Systems thinking prevents narrow optimization mistakes that shift work elsewhere. Learn how to map dependencies and avoid expensive AI implementation errors.",
|
||||||
|
type: "article",
|
||||||
|
url: "https://kbanc.com/claims-library/systems-thinking",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "Systems Thinking for AI Implementation",
|
||||||
|
description: "Systems thinking prevents narrow optimization mistakes that shift work elsewhere.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library/systems-thinking",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SystemsThinkingClaim() {
|
||||||
|
// JSON-LD schema for SEO
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: "Systems Thinking for AI Implementation",
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Kamil Banc",
|
||||||
|
jobTitle: "Publisher",
|
||||||
|
affiliation: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: "2025-11-03",
|
||||||
|
dateModified: "2025-11-03",
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
description: "Systems thinking prevents narrow optimization mistakes that shift work elsewhere. Five claims about avoiding expensive AI implementation errors.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* ========== HEADER ========== */}
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link
|
||||||
|
href="/claims-library"
|
||||||
|
className="inline-flex items-center text-sm text-primary hover:underline mb-4"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Claims Library
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">
|
||||||
|
Systems Thinking for AI Implementation
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
By{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Kamil Banc
|
||||||
|
</a>
|
||||||
|
, Publisher at AI Adopters Club
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/kbanc/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="LinkedIn Profile"
|
||||||
|
>
|
||||||
|
<Linkedin className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.youtube.com/@adaptandcreate"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="YouTube Channel"
|
||||||
|
>
|
||||||
|
<Youtube className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
{/* ========== ATOMIC CLAIMS SECTION ========== */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Atomic Claims</h2>
|
||||||
|
|
||||||
|
{/* Claim 1 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 1:</span> Preventing optimization mistakes
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Systems thinking prevents narrow optimization mistakes that shift work elsewhere.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 2 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 2:</span> Leverage points create wide impact
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Small targeted fixes in leverage points create system-wide improvements.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 3 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 3:</span> Root cause analysis
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
The iceberg model reveals root causes beneath symptoms and patterns.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 4 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 4:</span> Visual mapping exposes problems
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Process mapping on paper exposes bottlenecks and feedback loops.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Claim 5 */}
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 5:</span> Dependency surfacing
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Asking who else gets affected surfaces dependencies before they break.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== SUPPORTING EVIDENCE SECTION ========== */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Supporting Evidence</h2>
|
||||||
|
|
||||||
|
{/* Quote */}
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Quote</h3>
|
||||||
|
<blockquote className="italic text-lg leading-relaxed text-foreground border-l-4 border-l-accent pl-6 my-6">
|
||||||
|
<p>
|
||||||
|
"When you automate a process without mapping dependencies, you shift work
|
||||||
|
somewhere else."
|
||||||
|
</p>
|
||||||
|
<cite className="block not-italic text-sm text-muted-foreground mt-4">
|
||||||
|
Kamil Banc, AI Adopters Club
|
||||||
|
</cite>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sources */}
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Sources</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club/p/systems-thinking-ai-skill"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Systems thinking makes your AI skills actually useful (Original Article)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://verityai.co/blog/systems-thinking-ai-transformation-interconnected-impacts"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Verity AI: Systems Thinking in AI Transformation
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://www.abundance.global/systems-thinking-in-business-growth/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Abundance Global: Systems Thinking in Business Growth
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://hbr.org/2025/09/why-you-need-systems-thinking-now"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Harvard Business Review: Why You Need Systems Thinking Now
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://thesystemsthinker.com/systems-thinking-what-why-when-where-and-how/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
The Systems Thinker: What, Why, When, Where, and How
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== DOWNLOAD DATA SECTION ========== */}
|
||||||
|
<section className="bg-muted border-2 border-accent rounded-lg p-8 mb-12 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Download Data</h2>
|
||||||
|
<p className="mb-6">Access structured claim data in CSV format:</p>
|
||||||
|
<a href="/assets/systems-thinking-claims.csv" download>
|
||||||
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground transition-transform hover:translate-y-[-2px]">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download Claims Data (CSV)
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ========== CONTEXT SECTION ========== */}
|
||||||
|
<section className="bg-muted rounded-lg p-8 mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Context</h2>
|
||||||
|
<p className="mb-4 text-foreground">
|
||||||
|
These claims apply to AI implementation projects where teams automate processes
|
||||||
|
without understanding system-wide dependencies. Common scenarios include automating
|
||||||
|
workflows that create bottlenecks elsewhere, implementing AI tools without mapping
|
||||||
|
stakeholder impacts, optimizing individual metrics while degrading overall system
|
||||||
|
performance, and deploying solutions without understanding feedback loops.
|
||||||
|
</p>
|
||||||
|
<p className="text-foreground">
|
||||||
|
The framework helps professionals and consultants prevent expensive implementation
|
||||||
|
mistakes by mapping dependencies before automation. By applying systems thinking,
|
||||||
|
teams can identify leverage points where small changes create system-wide
|
||||||
|
improvements and avoid narrow optimizations that shift work to other parts of the
|
||||||
|
organization.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* ========== FOOTER ========== */}
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Content licensed under{" "}
|
||||||
|
<a
|
||||||
|
href="https://creativecommons.org/licenses/by/4.0/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
CC BY 4.0
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Source:{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
AI Adopters Club
|
||||||
|
</a>{" "}
|
||||||
|
by Kamil Banc
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club/p/work-with-kamil"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-accent hover:text-accent/80 transition-colors"
|
||||||
|
>
|
||||||
|
Need help with AI integration? → Work with Kamil
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { Download, ArrowLeft, Linkedin } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Make ChatGPT Writing Undetectable | Kamil Banc",
|
||||||
|
description: "Five techniques to make AI writing undetectable: active voice, varied sentence length, avoid clichés, minimal bullets, crisp endings.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Make ChatGPT Writing Undetectable With Five Techniques",
|
||||||
|
description: "Transform AI writing to sound natural using five proven techniques.",
|
||||||
|
type: "article",
|
||||||
|
url: "https://kbanc.com/claims-library/undetectable-writing",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "Make ChatGPT Writing Undetectable",
|
||||||
|
description: "Five techniques to make AI writing sound natural and undetectable.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library/undetectable-writing",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UndetectableWritingClaim() {
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: "Make ChatGPT Writing Undetectable With Five Techniques",
|
||||||
|
author: { "@type": "Person", name: "Kamil Banc", jobTitle: "Publisher", affiliation: { "@type": "Organization", name: "AI Adopters Club", url: "https://aiadopters.club" }},
|
||||||
|
datePublished: "2025-11-04",
|
||||||
|
dateModified: "2025-11-04",
|
||||||
|
publisher: { "@type": "Organization", name: "AI Adopters Club", url: "https://aiadopters.club" },
|
||||||
|
description: "Five claims about making AI writing sound human: active voice, varied sentence length, avoiding clichés, minimal bullets, and crisp endings."
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link href="/claims-library" className="inline-flex items-center text-sm text-primary hover:underline mb-4">
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />Back to Claims Library
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">Make ChatGPT Writing Undetectable</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-muted-foreground">By <a href="https://aiadopters.club" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Kamil Banc</a>, Publisher at AI Adopters Club</p>
|
||||||
|
<a href="https://www.linkedin.com/in/kbanc/" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors" aria-label="LinkedIn Profile"><Linkedin className="h-5 w-5" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Atomic Claims</h2>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 1:</span> Active voice masks AI authorship</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">Passive construction signals AI authorship while active voice sounds natural.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 2:</span> Sentence length variation prevents detection</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">Varying sentence length avoids the predictable metronome rhythm that betrays AI origins.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 3:</span> Clichéd phrases reveal automation</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">Stock phrases like navigate the landscape appear repeatedly in AI output.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 4:</span> Excessive bullets signal robots</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">AI defaults to unnecessary list formatting that marks robotic behavior.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3"><span className="text-accent">Claim 5:</span> Summary conclusions betray generation</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">Finishing with recaps marks mechanical composition versus conversational endings.</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Supporting Evidence</h2>
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Sources</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="pl-6 relative"><span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a href="https://aiadopters.club/p/theyll-never-know-chatgpt-wrote-this" target="_blank" rel="noopener" className="text-foreground hover:text-accent hover:underline transition-colors">
|
||||||
|
They'll Never Know ChatGPT Wrote This (Original Article)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-muted border-2 border-accent rounded-lg p-8 mb-12 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Download Data</h2>
|
||||||
|
<p className="mb-6">Access structured claim data in CSV format:</p>
|
||||||
|
<a href="/assets/undetectable-writing-claims.csv" download>
|
||||||
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground transition-transform hover:translate-y-[-2px]">
|
||||||
|
<Download className="mr-2 h-4 w-4" />Download Claims Data (CSV)
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-muted rounded-lg p-8 mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Context</h2>
|
||||||
|
<p className="text-foreground">These claims address how to transform AI-generated writing into natural-sounding prose. The techniques focus on eliminating mechanical patterns: using active voice instead of passive constructions, varying sentence length to avoid rhythmic predictability, removing clichéd phrases that saturate training data, minimizing unnecessary bullet points, and ending with crisp final lines rather than summary recaps.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">Content licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener" className="text-primary hover:underline">CC BY 4.0</a></p>
|
||||||
|
<p className="text-sm text-muted-foreground">Source: <a href="https://aiadopters.club" target="_blank" rel="noopener" className="text-primary hover:underline">AI Adopters Club</a> by Kamil Banc</p>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground"><a href="https://aiadopters.club/p/work-with-kamil" target="_blank" rel="noopener" className="text-accent hover:text-accent/80 transition-colors">Need help with AI integration? → Work with Kamil</a></p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
import { Download, ArrowLeft, Linkedin, Youtube } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Vibe Hackathons Transform AI Adoption in Three Hours",
|
||||||
|
description: "Vibe hackathons shift AI from abstract concept to daily tool in three hours. Cross-functional teams build prototypes and drive sustained usage through experiential learning.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Vibe Hackathons Transform AI Adoption in Three Hours",
|
||||||
|
description: "Vibe hackathons shift AI from abstract concept to daily tool in three hours. Cross-functional teams build prototypes and drive sustained usage.",
|
||||||
|
type: "article",
|
||||||
|
url: "https://kbanc.com/claims-library/vibe-hackathons",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "Vibe Hackathons Transform AI Adoption in Three Hours",
|
||||||
|
description: "Vibe hackathons shift AI from abstract concept to daily tool in three hours.",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/claims-library/vibe-hackathons",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function VibeHackathonsClaim() {
|
||||||
|
// JSON-LD schema for SEO
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: "Vibe Hackathons Transform AI Adoption in Three Hours",
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Kamil Banc",
|
||||||
|
jobTitle: "Author",
|
||||||
|
affiliation: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: "2025-11-02",
|
||||||
|
dateModified: "2025-11-02",
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Five atomic claims about how vibe hackathons accelerate AI adoption through experiential learning and cross-functional collaboration.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="bg-muted border-b-2 border-border py-8 mb-12">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<Link
|
||||||
|
href="/claims-library"
|
||||||
|
className="inline-flex items-center text-sm text-primary hover:underline mb-4"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Claims Library
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-foreground">
|
||||||
|
Vibe Hackathons Transform AI Adoption in Three Hours
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
By{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Kamil Banc
|
||||||
|
</a>
|
||||||
|
, Author at AI Adopters Club
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/kbanc/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="LinkedIn Profile"
|
||||||
|
>
|
||||||
|
<Linkedin className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.youtube.com/@adaptandcreate"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:text-primary/80 transition-colors"
|
||||||
|
aria-label="YouTube Channel"
|
||||||
|
>
|
||||||
|
<Youtube className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-3xl mx-auto px-6 pb-12">
|
||||||
|
{/* Claims Section */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Atomic Claims</h2>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 1:</span> Rapid transformation through experiential learning
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Vibe hackathons shift AI from abstract concept to daily tool in three hours.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 2:</span> Cross-functional teams discover overlooked opportunities
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Mixed teams combining technical and non-technical staff identify automation
|
||||||
|
opportunities developers miss.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 3:</span> Leadership participation signals organizational support
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Executive participation in hackathons signals support for experimentation and
|
||||||
|
surfaces friction points.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 4:</span> Experiential learning drives sustained usage
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
ChatGPT usage doubles the week after hackathons because people experience
|
||||||
|
creation satisfaction.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="bg-muted border border-border border-l-4 border-l-accent rounded-lg p-6 mb-6 transition-transform hover:translate-y-[-2px] hover:shadow-md">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
<span className="text-accent">Claim 5:</span> Accessible tools enable rapid prototyping
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg font-medium leading-relaxed text-foreground">
|
||||||
|
Single-page prototypes with no databases can be built in two to four hours.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Supporting Evidence Section */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 mt-12">Supporting Evidence</h2>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Quote</h3>
|
||||||
|
<blockquote className="italic text-lg leading-relaxed text-foreground border-l-4 border-l-accent pl-6 my-6">
|
||||||
|
<p>
|
||||||
|
"When people make something that solves their own problem, they return to AI
|
||||||
|
the next day."
|
||||||
|
</p>
|
||||||
|
<cite className="block not-italic text-sm text-muted-foreground mt-4">
|
||||||
|
Kamil Banc, AI Adopters Club
|
||||||
|
</cite>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Key Statistic</h3>
|
||||||
|
<p className="text-xl font-semibold text-accent bg-muted p-4 rounded-lg">
|
||||||
|
ChatGPT usage doubles the week after a hackathon
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Sources</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club/p/ai-hackathon"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Why Your Company Should Run A Vibe-Hackathon (Original Article)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://academy.openai.com/public/clubs/champions-ecqup/resources/hackathon-playbook-2025-09-15"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
OpenAI Hackathon Playbook 2025
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="pl-6 relative">
|
||||||
|
<span className="absolute left-0 text-accent font-bold">→</span>
|
||||||
|
<a
|
||||||
|
href="https://aistudio.google.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-foreground hover:text-accent hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Gemini Build (Vibe Coding Platform)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Download Section */}
|
||||||
|
<section className="bg-muted border-2 border-accent rounded-lg p-8 mb-12 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Download Data</h2>
|
||||||
|
<p className="mb-6">Access structured claim data in CSV format:</p>
|
||||||
|
<a href="/assets/data-template.csv" download>
|
||||||
|
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground transition-transform hover:translate-y-[-2px]">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download Claims Data (CSV)
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Context Section */}
|
||||||
|
<section className="bg-muted rounded-lg p-8 mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Context</h2>
|
||||||
|
<p className="mb-4 text-foreground">
|
||||||
|
This page presents atomic claims extracted from research on vibe coding
|
||||||
|
hackathons as a method for accelerating AI adoption in organizations. Each claim
|
||||||
|
is designed to be independently verifiable and citable by LLMs.
|
||||||
|
</p>
|
||||||
|
<p className="text-foreground">
|
||||||
|
The methodology focuses on experiential learning through hands-on prototype
|
||||||
|
building, cross-functional collaboration, and rapid three-hour sprints that
|
||||||
|
transform theoretical AI knowledge into practical tool usage.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-muted border-t-2 border-border py-8 mt-12 text-center">
|
||||||
|
<div className="container max-w-3xl mx-auto px-6">
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Content licensed under{" "}
|
||||||
|
<a
|
||||||
|
href="https://creativecommons.org/licenses/by/4.0/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
CC BY 4.0
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Source:{" "}
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
AI Adopters Club
|
||||||
|
</a>{" "}
|
||||||
|
by Kamil Banc
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club/p/work-with-kamil"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="text-accent hover:text-accent/80 transition-colors"
|
||||||
|
>
|
||||||
|
Need help with AI integration? → Work with Kamil
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
||||||
|
All colors MUST be HSL.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 0 0% 10%;
|
||||||
|
|
||||||
|
--card: 0 0% 98%;
|
||||||
|
--card-foreground: 0 0% 10%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 0 0% 10%;
|
||||||
|
|
||||||
|
--primary: 0 0% 10%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--secondary: 0 0% 40%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--muted: 0 0% 96%;
|
||||||
|
--muted-foreground: 0 0% 40%;
|
||||||
|
|
||||||
|
--accent: 0 70% 50%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 70% 50%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 0 0% 90%;
|
||||||
|
--input: 0 0% 90%;
|
||||||
|
--ring: 0 70% 50%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
--sidebar-background: 0 0% 98%;
|
||||||
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
|
--sidebar-border: 220 13% 91%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 10%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--card: 0 0% 15%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--popover: 0 0% 10%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 0 0% 10%;
|
||||||
|
|
||||||
|
--secondary: 0 0% 40%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--muted: 0 0% 20%;
|
||||||
|
--muted-foreground: 0 0% 60%;
|
||||||
|
|
||||||
|
--accent: 0 70% 50%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 70% 50%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 0 0% 20%;
|
||||||
|
--input: 0 0% 20%;
|
||||||
|
--ring: 0 70% 50%;
|
||||||
|
--sidebar-background: 240 5.9% 10%;
|
||||||
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Kamil Banc | AI Implementation Expert",
|
||||||
|
description: "AI implementation insights and frameworks from Kamil Banc. Citation-ready claims optimized for LLM extraction.",
|
||||||
|
authors: [{ name: "Kamil Banc" }],
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
url: "https://kbanc.com/",
|
||||||
|
title: "Kamil Banc | AI Implementation Expert",
|
||||||
|
description: "AI implementation insights and frameworks from Kamil Banc. Citation-ready claims optimized for LLM extraction.",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "https://kbanc.com/kamil-banc-og.jpg",
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: "Kamil Banc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@kamilbanc",
|
||||||
|
creator: "@kamilbanc",
|
||||||
|
title: "Kamil Banc | AI Implementation Expert",
|
||||||
|
description: "AI implementation insights and frameworks from Kamil Banc. Citation-ready claims optimized for LLM extraction.",
|
||||||
|
images: ["https://kbanc.com/kamil-banc-og.jpg"],
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "https://kbanc.com/",
|
||||||
|
types: {
|
||||||
|
'application/rss+xml': '/feed.xml',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: "/favicon.png",
|
||||||
|
apple: "/favicon.png",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="preconnect" href="https://calendly.com" crossOrigin="anonymous" />
|
||||||
|
<link rel="preconnect" href="https://aiadopters.club" crossOrigin="anonymous" />
|
||||||
|
<link rel="dns-prefetch" href="https://calendly.com" />
|
||||||
|
<link rel="dns-prefetch" href="https://aiadopters.club" />
|
||||||
|
</head>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import Header from "@/components/Header";
|
||||||
|
import Bio from "@/components/Bio";
|
||||||
|
import Services from "@/components/Services";
|
||||||
|
import ClaimsLibrary from "@/components/ClaimsLibrary";
|
||||||
|
import UpdateGuide from "@/components/UpdateGuide";
|
||||||
|
import Footer from "@/components/Footer";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Kamil Banc | AI Implementation Expert",
|
||||||
|
description: "AI implementation expert helping organizations transform AI from decorative art into practical business machinery with measurable ROI.",
|
||||||
|
};
|
||||||
|
|
||||||
|
// JSON-LD structured data
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Kamil Banc",
|
||||||
|
jobTitle: "AI Implementation Expert",
|
||||||
|
description: "AI implementation expert helping organizations transform AI from decorative art into practical business machinery with measurable ROI.",
|
||||||
|
url: "https://kbanc.com",
|
||||||
|
image: "https://kbanc.com/kamil-banc.jpg",
|
||||||
|
sameAs: [
|
||||||
|
"https://www.linkedin.com/in/kbanc/",
|
||||||
|
"https://aiadopters.club",
|
||||||
|
"https://x.com/kamilbanc"
|
||||||
|
],
|
||||||
|
knowsAbout: [
|
||||||
|
"Artificial Intelligence",
|
||||||
|
"AI Implementation",
|
||||||
|
"AI Strategy",
|
||||||
|
"Digital Transformation",
|
||||||
|
"AI Adoption"
|
||||||
|
],
|
||||||
|
affiliation: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "AI Adopters Club",
|
||||||
|
url: "https://aiadopters.club"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-background">
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
|
<Header />
|
||||||
|
<main id="main-content">
|
||||||
|
<Bio />
|
||||||
|
<ClaimsLibrary />
|
||||||
|
<UpdateGuide />
|
||||||
|
<Services />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
|
||||||
|
const Bio = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-10 md:py-12 border-b border-border">
|
||||||
|
<div className="container max-w-[780px] mx-auto px-5">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-base leading-relaxed">
|
||||||
|
I help organizations implement AI systems that actually work. Not theoretical frameworks or vendor pitches—real implementations that generate measurable value.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-base leading-relaxed">
|
||||||
|
My work focuses on practical adoption patterns: what works, what fails, and why. I document implementation frameworks, measure outcomes, and share findings through the AI Adopters Club community.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-base leading-relaxed">
|
||||||
|
This library contains citation-ready claims from my research and consulting work. Each claim is backed by data, includes context, and is optimized for LLM extraction.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-4 pt-2">
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/kamilbanc"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-accent hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
LinkedIn <ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://aiadopters.club"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-accent hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
AI Adopters Club <ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bio;
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const ClaimsLibrary = () => {
|
||||||
|
const claims = [
|
||||||
|
{
|
||||||
|
title: "Vibe Hackathons Transform AI Adoption in Three Hours",
|
||||||
|
link: "/claims-library/vibe-hackathons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Systems Thinking for AI Implementation",
|
||||||
|
link: "/claims-library/systems-thinking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "AI ROI Measurement: Why 95% See Zero Returns",
|
||||||
|
link: "/claims-library/ai-roi-measurement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "How to Set Up ChatGPT Properly in Under 10 Minutes",
|
||||||
|
link: "/claims-library/chatgpt-setup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "AI Prompt Maps Employee Skill Gaps in One Session",
|
||||||
|
link: "/claims-library/skill-gap-mapping"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Make ChatGPT Writing Undetectable With Five Techniques",
|
||||||
|
link: "/claims-library/undetectable-writing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Alpha School: Two Hours of AI-Led Learning Beats Full Day of Classes",
|
||||||
|
link: "/claims-library/alpha-school"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Run $150K Market Entry Study in 20 Minutes With AI Research Prompts",
|
||||||
|
link: "/claims-library/market-entry-study"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-10 md:py-12 border-b border-border">
|
||||||
|
<div className="container max-w-[780px] mx-auto px-5">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold">Claims Library</h2>
|
||||||
|
<Link
|
||||||
|
href="/claims-library"
|
||||||
|
className="text-sm font-medium text-accent hover:text-accent/80 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
View Full Library →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{claims.map((claim, idx) => (
|
||||||
|
<div key={idx} className="border-l-4 border-l-accent bg-muted p-4 rounded hover:shadow-md transition-shadow">
|
||||||
|
<Link href={claim.link} className="text-base font-medium text-foreground hover:text-accent hover:underline transition-colors">
|
||||||
|
{claim.title}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClaimsLibrary;
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Component, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
console.error('Error caught by boundary:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-6">
|
||||||
|
<div className="max-w-md text-center space-y-4">
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Something went wrong</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
We've encountered an unexpected error. Please try refreshing the page.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="px-4 py-2 bg-accent text-accent-foreground rounded hover:bg-accent/90 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Refresh Page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<footer className="py-8 md:py-10">
|
||||||
|
<div className="container max-w-[780px] mx-auto px-5">
|
||||||
|
<div className="flex flex-col items-center space-y-6">
|
||||||
|
<img
|
||||||
|
src="/ai-adopters-mug.gif"
|
||||||
|
alt="AI Adopters Club"
|
||||||
|
className="w-64 h-auto"
|
||||||
|
/>
|
||||||
|
<div className="text-sm text-muted-foreground space-y-2 text-center">
|
||||||
|
<p>
|
||||||
|
© {new Date().getFullYear()} Kamil Banc. Licensed under{" "}
|
||||||
|
<a
|
||||||
|
href="https://creativecommons.org/licenses/by/4.0/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-accent hover:underline"
|
||||||
|
>
|
||||||
|
Creative Commons Attribution 4.0
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Attribution required: "Kamil Banc, AI Adopters Club" with link to this site.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
const Header = () => {
|
||||||
|
return (
|
||||||
|
<header className="border-b border-border py-12 md:py-16">
|
||||||
|
<div className="container max-w-[780px] mx-auto px-5">
|
||||||
|
<div className="flex flex-row gap-6 items-center">
|
||||||
|
<img
|
||||||
|
src="/kamil-banc.jpg"
|
||||||
|
alt="Kamil Banc"
|
||||||
|
className="w-24 h-24 md:w-32 md:h-32 rounded-lg object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold mb-2">Kamil Banc</h1>
|
||||||
|
<p className="text-xl text-secondary">AI Implementation Expert</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
const Services = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-10 md:py-12 border-t border-border">
|
||||||
|
<div className="container max-w-[780px] mx-auto px-5">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<p className="text-base leading-relaxed text-muted-foreground">
|
||||||
|
I work with organizations on AI implementation through discovery calls, audits, advisory, and fractional leadership.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="https://calendly.com/kbanc/ai?month=2025-11"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-block px-5 py-2 border border-accent text-accent rounded hover:bg-accent hover:text-accent-foreground transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Book Discovery Call
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Services;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
const SkipToContent = () => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="#main-content"
|
||||||
|
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-accent focus:text-accent-foreground focus:rounded focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SkipToContent;
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
const UpdateGuide = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-10 md:py-12 border-b border-border">
|
||||||
|
<div className="container max-w-[780px] mx-auto px-5">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold mb-6">How This Works</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4 text-base leading-relaxed">
|
||||||
|
<p>
|
||||||
|
This site is optimized for AI extraction. Each claim page follows the Google-Extended-Optimized (GEO) format, making it easy for LLMs to cite my work accurately.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The structure is intentionally simple: claims, evidence, context, and attribution. No complex navigation, no dynamic content that breaks crawlers, no barriers to citation.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-card p-6 rounded-lg mt-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">For AI Researchers & Developers</h3>
|
||||||
|
<p className="mb-3">
|
||||||
|
You're welcome to use this content in your AI training or research. All content is licensed under Creative Commons Attribution 4.0.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Requirements: Attribute to "Kamil Banc, AI Adopters Club" and include a link back to this site.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateGuide;
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root;
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
||||||
|
));
|
||||||
|
AccordionItem.displayName = "AccordionItem";
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
));
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
));
|
||||||
|
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root;
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
));
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||||
|
);
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader";
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||||
|
);
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter";
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
||||||
|
));
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
|
));
|
||||||
|
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||||
|
));
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
||||||
|
));
|
||||||
|
Alert.displayName = "Alert";
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
AlertTitle.displayName = "AlertTitle";
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
AlertDescription.displayName = "AlertDescription";
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||||
|
|
||||||
|
const AspectRatio = AspectRatioPrimitive.Root;
|
||||||
|
|
||||||
|
export { AspectRatio };
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Avatar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
|
||||||
|
));
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Breadcrumb = React.forwardRef<
|
||||||
|
HTMLElement,
|
||||||
|
React.ComponentPropsWithoutRef<"nav"> & {
|
||||||
|
separator?: React.ReactNode;
|
||||||
|
}
|
||||||
|
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
||||||
|
Breadcrumb.displayName = "Breadcrumb";
|
||||||
|
|
||||||
|
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<ol
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
BreadcrumbList.displayName = "BreadcrumbList";
|
||||||
|
|
||||||
|
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||||
|
|
||||||
|
const BreadcrumbLink = React.forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
React.ComponentPropsWithoutRef<"a"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
>(({ asChild, className, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
|
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
|
||||||
|
});
|
||||||
|
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||||
|
|
||||||
|
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("font-normal text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||||
|
|
||||||
|
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
||||||
|
<li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||||
|
|
||||||
|
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
|
||||||
|
));
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
|
||||||
|
);
|
||||||
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
));
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Checkbox };
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||||
|
|
||||||
|
const Collapsible = CollapsiblePrimitive.Root;
|
||||||
|
|
||||||
|
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||||
|
|
||||||
|
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||||
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
const Command = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Command.displayName = CommandPrimitive.displayName;
|
||||||
|
|
||||||
|
interface CommandDialogProps extends DialogProps {}
|
||||||
|
|
||||||
|
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommandInput = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||||
|
|
||||||
|
const CommandList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const CommandEmpty = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
|
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
|
||||||
|
|
||||||
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||||
|
|
||||||
|
const CommandGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||||
|
|
||||||
|
const CommandSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
|
||||||
|
));
|
||||||
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const CommandItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
||||||
|
};
|
||||||
|
CommandShortcut.displayName = "CommandShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const ContextMenu = ContextMenuPrimitive.Root;
|
||||||
|
|
||||||
|
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||||
|
|
||||||
|
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||||
|
|
||||||
|
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||||
|
|
||||||
|
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||||
|
|
||||||
|
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
|
const ContextMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
));
|
||||||
|
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
|
const ContextMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
|
const ContextMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
));
|
||||||
|
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const ContextMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const ContextMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
const ContextMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
));
|
||||||
|
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
|
const ContextMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const ContextMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
|
||||||
|
));
|
||||||
|
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
||||||
|
};
|
||||||
|
ContextMenuShortcut.displayName = "ContextMenuShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal;
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
));
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||||
|
);
|
||||||
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
|
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||||
|
);
|
||||||
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
|
));
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent focus:bg-accent",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
));
|
||||||
|
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
));
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
));
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
||||||
|
));
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
|
||||||
|
};
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
const Form = FormProvider;
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
|
const itemContext = React.useContext(FormItemContext);
|
||||||
|
const { getFieldState, formState } = useFormContext();
|
||||||
|
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
|
||||||
|
|
||||||
|
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
const id = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
FormItem.displayName = "FormItem";
|
||||||
|
|
||||||
|
const FormLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
|
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
|
||||||
|
});
|
||||||
|
FormLabel.displayName = "FormLabel";
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
|
||||||
|
({ ...props }, ref) => {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
ref={ref}
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
FormControl.displayName = "FormControl";
|
||||||
|
|
||||||
|
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
|
return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
FormDescription.displayName = "FormDescription";
|
||||||
|
|
||||||
|
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
const { error, formMessageId } = useFormField();
|
||||||
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
FormMessage.displayName = "FormMessage";
|
||||||
|
|
||||||
|
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const HoverCard = HoverCardPrimitive.Root;
|
||||||
|
|
||||||
|
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||||
|
|
||||||
|
const HoverCardContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<HoverCardPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||||
|
));
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Label };
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const MenubarMenu = MenubarPrimitive.Menu;
|
||||||
|
|
||||||
|
const MenubarGroup = MenubarPrimitive.Group;
|
||||||
|
|
||||||
|
const MenubarPortal = MenubarPrimitive.Portal;
|
||||||
|
|
||||||
|
const MenubarSub = MenubarPrimitive.Sub;
|
||||||
|
|
||||||
|
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||||
|
|
||||||
|
const Menubar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex h-10 items-center space-x-1 rounded-md border bg-background p-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const MenubarTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const MenubarSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</MenubarPrimitive.SubTrigger>
|
||||||
|
));
|
||||||
|
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
|
const MenubarSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
|
const MenubarContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||||
|
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Portal>
|
||||||
|
<MenubarPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
alignOffset={alignOffset}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</MenubarPrimitive.Portal>
|
||||||
|
));
|
||||||
|
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const MenubarItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const MenubarCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<MenubarPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</MenubarPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</MenubarPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
const MenubarRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<MenubarPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</MenubarPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</MenubarPrimitive.RadioItem>
|
||||||
|
));
|
||||||
|
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
|
const MenubarLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const MenubarSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
||||||
|
));
|
||||||
|
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
||||||
|
};
|
||||||
|
MenubarShortcut.displayname = "MenubarShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Menubar,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarTrigger,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarSeparator,
|
||||||
|
MenubarLabel,
|
||||||
|
MenubarCheckboxItem,
|
||||||
|
MenubarRadioGroup,
|
||||||
|
MenubarRadioItem,
|
||||||
|
MenubarPortal,
|
||||||
|
MenubarSubContent,
|
||||||
|
MenubarSubTrigger,
|
||||||
|
MenubarGroup,
|
||||||
|
MenubarSub,
|
||||||
|
MenubarShortcut,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const NavigationMenu = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<NavigationMenuViewport />
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
));
|
||||||
|
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const NavigationMenuList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||||
|
);
|
||||||
|
|
||||||
|
const NavigationMenuTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDown
|
||||||
|
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
));
|
||||||
|
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const NavigationMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||||
|
|
||||||
|
const NavigationMenuViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
|
||||||
|
|
||||||
|
const NavigationMenuIndicator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
));
|
||||||
|
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ButtonProps, buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
Pagination.displayName = "Pagination";
|
||||||
|
|
||||||
|
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
PaginationContent.displayName = "PaginationContent";
|
||||||
|
|
||||||
|
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
|
||||||
|
<li ref={ref} className={cn("", className)} {...props} />
|
||||||
|
));
|
||||||
|
PaginationItem.displayName = "PaginationItem";
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
} & Pick<ButtonProps, "size"> &
|
||||||
|
React.ComponentProps<"a">;
|
||||||
|
|
||||||
|
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
PaginationLink.displayName = "PaginationLink";
|
||||||
|
|
||||||
|
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
|
<PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 pl-2.5", className)} {...props}>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
<span>Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
PaginationPrevious.displayName = "PaginationPrevious";
|
||||||
|
|
||||||
|
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
|
<PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 pr-2.5", className)} {...props}>
|
||||||
|
<span>Next</span>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
PaginationNext.displayName = "PaginationNext";
|
||||||
|
|
||||||
|
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
||||||
|
<span aria-hidden className={cn("flex h-9 w-9 items-center justify-center", className)} {...props}>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
PaginationEllipsis.displayName = "PaginationEllipsis";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
));
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent };
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
));
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Progress };
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
|
import { Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
|
||||||
|
});
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem };
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { GripVertical } from "lucide-react";
|
||||||
|
import * as ResizablePrimitive from "react-resizable-panels";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||||
|
<ResizablePrimitive.PanelGroup
|
||||||
|
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ResizablePanel = ResizablePrimitive.Panel;
|
||||||
|
|
||||||
|
const ResizableHandle = ({
|
||||||
|
withHandle,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||||
|
withHandle?: boolean;
|
||||||
|
}) => (
|
||||||
|
<ResizablePrimitive.PanelResizeHandle
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{withHandle && (
|
||||||
|
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||||
|
<GripVertical className="h-2.5 w-2.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ResizablePrimitive.PanelResizeHandle>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
));
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none select-none transition-colors",
|
||||||
|
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||||
|
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
));
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar };
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root;
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group;
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value;
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
));
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
));
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
));
|
||||||
|
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
));
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
|
||||||
|
));
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
));
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
||||||
|
));
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Sheet = SheetPrimitive.Root;
|
||||||
|
|
||||||
|
const SheetTrigger = SheetPrimitive.Trigger;
|
||||||
|
|
||||||
|
const SheetClose = SheetPrimitive.Close;
|
||||||
|
|
||||||
|
const SheetPortal = SheetPrimitive.Portal;
|
||||||
|
|
||||||
|
const SheetOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const sheetVariants = cva(
|
||||||
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
side: {
|
||||||
|
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||||
|
bottom:
|
||||||
|
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||||
|
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||||
|
right:
|
||||||
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SheetContentProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
|
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
|
||||||
|
({ side = "right", className, children, ...props }, ref) => (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||||
|
{children}
|
||||||
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||||
|
);
|
||||||
|
SheetHeader.displayName = "SheetHeader";
|
||||||
|
|
||||||
|
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||||
|
);
|
||||||
|
SheetFooter.displayName = "SheetFooter";
|
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
|
||||||
|
));
|
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
|
));
|
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetOverlay,
|
||||||
|
SheetPortal,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,637 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { VariantProps, cva } from "class-variance-authority";
|
||||||
|
import { PanelLeft } from "lucide-react";
|
||||||
|
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
const SIDEBAR_COOKIE_NAME = "sidebar:state";
|
||||||
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||||
|
const SIDEBAR_WIDTH = "16rem";
|
||||||
|
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||||
|
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||||
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||||
|
|
||||||
|
type SidebarContext = {
|
||||||
|
state: "expanded" | "collapsed";
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
openMobile: boolean;
|
||||||
|
setOpenMobile: (open: boolean) => void;
|
||||||
|
isMobile: boolean;
|
||||||
|
toggleSidebar: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarContext = React.createContext<SidebarContext | null>(null);
|
||||||
|
|
||||||
|
function useSidebar() {
|
||||||
|
const context = React.useContext(SidebarContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarProvider = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [openMobile, setOpenMobile] = React.useState(false);
|
||||||
|
|
||||||
|
// This is the internal state of the sidebar.
|
||||||
|
// We use openProp and setOpenProp for control from outside the component.
|
||||||
|
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||||
|
const open = openProp ?? _open;
|
||||||
|
const setOpen = React.useCallback(
|
||||||
|
(value: boolean | ((value: boolean) => boolean)) => {
|
||||||
|
const openState = typeof value === "function" ? value(open) : value;
|
||||||
|
if (setOpenProp) {
|
||||||
|
setOpenProp(openState);
|
||||||
|
} else {
|
||||||
|
_setOpen(openState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This sets the cookie to keep the sidebar state.
|
||||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||||
|
},
|
||||||
|
[setOpenProp, open],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper to toggle the sidebar.
|
||||||
|
const toggleSidebar = React.useCallback(() => {
|
||||||
|
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||||
|
}, [isMobile, setOpen, setOpenMobile]);
|
||||||
|
|
||||||
|
// Adds a keyboard shortcut to toggle the sidebar.
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [toggleSidebar]);
|
||||||
|
|
||||||
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||||
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
|
const state = open ? "expanded" : "collapsed";
|
||||||
|
|
||||||
|
const contextValue = React.useMemo<SidebarContext>(
|
||||||
|
() => ({
|
||||||
|
state,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isMobile,
|
||||||
|
openMobile,
|
||||||
|
setOpenMobile,
|
||||||
|
toggleSidebar,
|
||||||
|
}),
|
||||||
|
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider value={contextValue}>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--sidebar-width": SIDEBAR_WIDTH,
|
||||||
|
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||||
|
...style,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", className)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarProvider.displayName = "SidebarProvider";
|
||||||
|
|
||||||
|
const Sidebar = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
side?: "left" | "right";
|
||||||
|
variant?: "sidebar" | "floating" | "inset";
|
||||||
|
collapsible?: "offcanvas" | "icon" | "none";
|
||||||
|
}
|
||||||
|
>(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
|
||||||
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
|
if (collapsible === "none") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", className)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||||
|
<SheetContent
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-mobile="true"
|
||||||
|
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
side={side}
|
||||||
|
>
|
||||||
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="group peer hidden text-sidebar-foreground md:block"
|
||||||
|
data-state={state}
|
||||||
|
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||||
|
data-variant={variant}
|
||||||
|
data-side={side}
|
||||||
|
>
|
||||||
|
{/* This is what handles the sidebar gap on desktop */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||||
|
"group-data-[collapsible=offcanvas]:w-0",
|
||||||
|
"group-data-[side=right]:rotate-180",
|
||||||
|
variant === "floating" || variant === "inset"
|
||||||
|
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||||
|
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||||
|
side === "left"
|
||||||
|
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||||
|
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||||
|
// Adjust the padding for floating and inset variants.
|
||||||
|
variant === "floating" || variant === "inset"
|
||||||
|
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||||
|
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Sidebar.displayName = "Sidebar";
|
||||||
|
|
||||||
|
const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
|
||||||
|
({ className, onClick, ...props }, ref) => {
|
||||||
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="trigger"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn("h-7 w-7", className)}
|
||||||
|
onClick={(event) => {
|
||||||
|
onClick?.(event);
|
||||||
|
toggleSidebar();
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<PanelLeft />
|
||||||
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
SidebarTrigger.displayName = "SidebarTrigger";
|
||||||
|
|
||||||
|
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button">>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="rail"
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
title="Toggle Sidebar"
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 hover:after:bg-sidebar-border sm:flex",
|
||||||
|
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||||
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||||
|
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
||||||
|
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||||
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
SidebarRail.displayName = "SidebarRail";
|
||||||
|
|
||||||
|
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<"main">>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex min-h-svh flex-1 flex-col bg-background",
|
||||||
|
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarInset.displayName = "SidebarInset";
|
||||||
|
|
||||||
|
const SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, React.ComponentProps<typeof Input>>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="input"
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
SidebarInput.displayName = "SidebarInput";
|
||||||
|
|
||||||
|
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
|
||||||
|
return <div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
|
||||||
|
});
|
||||||
|
SidebarHeader.displayName = "SidebarHeader";
|
||||||
|
|
||||||
|
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
|
||||||
|
return <div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
|
||||||
|
});
|
||||||
|
SidebarFooter.displayName = "SidebarFooter";
|
||||||
|
|
||||||
|
const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Separator
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="separator"
|
||||||
|
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
SidebarSeparator.displayName = "SidebarSeparator";
|
||||||
|
|
||||||
|
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="content"
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarContent.displayName = "SidebarContent";
|
||||||
|
|
||||||
|
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="group"
|
||||||
|
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarGroup.displayName = "SidebarGroup";
|
||||||
|
|
||||||
|
const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<"div"> & { asChild?: boolean }>(
|
||||||
|
({ className, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "div";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="group-label"
|
||||||
|
className={cn(
|
||||||
|
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||||
|
|
||||||
|
const SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button"> & { asChild?: boolean }>(
|
||||||
|
({ className, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="group-action"
|
||||||
|
className={cn(
|
||||||
|
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 after:md:hidden",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||||
|
|
||||||
|
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
SidebarGroupContent.displayName = "SidebarGroupContent";
|
||||||
|
|
||||||
|
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(({ className, ...props }, ref) => (
|
||||||
|
<ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} />
|
||||||
|
));
|
||||||
|
SidebarMenu.displayName = "SidebarMenu";
|
||||||
|
|
||||||
|
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
|
||||||
|
<li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
|
||||||
|
));
|
||||||
|
SidebarMenuItem.displayName = "SidebarMenuItem";
|
||||||
|
|
||||||
|
const sidebarMenuButtonVariants = cva(
|
||||||
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
|
outline:
|
||||||
|
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-8 text-sm",
|
||||||
|
sm: "h-7 text-xs",
|
||||||
|
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const SidebarMenuButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<"button"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||||
|
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||||
|
>(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
const { isMobile, state } = useSidebar();
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-button"
|
||||||
|
data-size={size}
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tooltip) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tooltip === "string") {
|
||||||
|
tooltip = {
|
||||||
|
children: tooltip,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||||
|
|
||||||
|
const SidebarMenuAction = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<"button"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
showOnHover?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-action"
|
||||||
|
className={cn(
|
||||||
|
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 after:md:hidden",
|
||||||
|
"peer-data-[size=sm]/menu-button:top-1",
|
||||||
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
showOnHover &&
|
||||||
|
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||||
|
|
||||||
|
const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
|
||||||
|
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||||
|
"peer-data-[size=sm]/menu-button:top-1",
|
||||||
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||||
|
|
||||||
|
const SidebarMenuSkeleton = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
showIcon?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, showIcon = false, ...props }, ref) => {
|
||||||
|
// Random width between 50 to 90%.
|
||||||
|
const width = React.useMemo(() => {
|
||||||
|
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-skeleton"
|
||||||
|
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
|
||||||
|
<Skeleton
|
||||||
|
className="h-4 max-w-[--skeleton-width] flex-1"
|
||||||
|
data-sidebar="menu-skeleton-text"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--skeleton-width": width,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
||||||
|
|
||||||
|
const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<ul
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-sub"
|
||||||
|
className={cn(
|
||||||
|
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
SidebarMenuSub.displayName = "SidebarMenuSub";
|
||||||
|
|
||||||
|
const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ ...props }, ref) => (
|
||||||
|
<li ref={ref} {...props} />
|
||||||
|
));
|
||||||
|
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
||||||
|
|
||||||
|
const SidebarMenuSubButton = React.forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
size?: "sm" | "md";
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
data-sidebar="menu-sub-button"
|
||||||
|
data-size={size}
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||||
|
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||||
|
size === "sm" && "text-xs",
|
||||||
|
size === "md" && "text-sm",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupAction,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarInput,
|
||||||
|
SidebarInset,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuBadge,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSkeleton,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
SidebarProvider,
|
||||||
|
SidebarRail,
|
||||||
|
SidebarSeparator,
|
||||||
|
SidebarTrigger,
|
||||||
|
useSidebar,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton };
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Slider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
));
|
||||||
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Slider };
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { Toaster as Sonner, toast } from "sonner";
|
||||||
|
|
||||||
|
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||||
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
|
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
|
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Toaster, toast };
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
));
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||||
|
|
||||||
|
export { Switch };
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Table.displayName = "Table";
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||||
|
({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />,
|
||||||
|
);
|
||||||
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
|
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;
|
||||||
|
});
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors group-[.destructive]:border-muted/40 hover:bg-secondary group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 group-[.destructive]:focus:ring-destructive disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:outline-none focus:ring-2 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
));
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
|
||||||
|
));
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
||||||
|
));
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast";
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && <ToastDescription>{description}</ToastDescription>}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||||
|
import { type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { toggleVariants } from "@/components/ui/toggle";
|
||||||
|
|
||||||
|
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
|
||||||
|
size: "default",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ToggleGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, variant, size, children, ...props }, ref) => (
|
||||||
|
<ToggleGroupPrimitive.Root ref={ref} className={cn("flex items-center justify-center gap-1", className)} {...props}>
|
||||||
|
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive.Root>
|
||||||
|
));
|
||||||
|
|
||||||
|
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const ToggleGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, children, variant, size, ...props }, ref) => {
|
||||||
|
const context = React.useContext(ToggleGroupContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem };
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-3",
|
||||||
|
sm: "h-9 px-2.5",
|
||||||
|
lg: "h-11 px-5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Toggle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, variant, size, ...props }, ref) => (
|
||||||
|
<TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />
|
||||||
|
));
|
||||||
|
|
||||||
|
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants };
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider;
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root;
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { useToast, toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
export { useToast, toast };
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
export function useIsMobile() {
|
||||||
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||||
|
const onChange = () => {
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
|
};
|
||||||
|
mql.addEventListener("change", onChange);
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
|
return () => mql.removeEventListener("change", onChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return !!isMobile;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1;
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000;
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
action?: ToastActionElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||||
|
return count.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes;
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"];
|
||||||
|
toast: ToasterToast;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"];
|
||||||
|
toast: Partial<ToasterToast>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"];
|
||||||
|
toastId?: ToasterToast["id"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"];
|
||||||
|
toastId?: ToasterToast["id"];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId);
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
});
|
||||||
|
}, TOAST_REMOVE_DELAY);
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action;
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId);
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = [];
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] };
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action);
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">;
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId();
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
});
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState);
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState);
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast };
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||