@@ -30,6 +30,7 @@ Do not change any of these without explicit instruction from the user.
3030| Blog editor | TipTap (rich text, stores JSON) |
3131| File storage | Cloudinary |
3232| Email | Resend |
33+ | Image cropping | react-image-crop (admin only — never on public pages) |
3334| Fonts | JetBrains Mono + IBM Plex Sans |
3435| Hosting | Vercel |
3536| Package manager | pnpm — NEVER use npm or yarn |
@@ -105,7 +106,7 @@ Footer: bg-[#F4F5F8] border-t border-[#C4CAD6]
105106- ** Light theme only.** No dark mode. No `dark:` Tailwind variants.
106107- ** No animations.** Nothing moves. No keyframes, no motion libraries.
107108- ** Minimal hover effects.** Color or opacity changes only.
108- - ** No UI libraries.** Build everything from scratch with Tailwind.
109+ - ** No UI component libraries.** Build everything from scratch with Tailwind.
109110- ** No gradients.** Solid colors only.
110111- ** Use borders sparingly.** Prefer spacing and background contrast.
111112- ** No shadows** except subtle `shadow-sm ` on cards where needed.
@@ -124,28 +125,20 @@ Logo files in `public/logos/`:
124125### Icons
125126
126127- ** Never install an icon library** (no lucide-react , heroicons package, react-icons , etc.)
127- - All icons are ** inline SVGs** written directly in the component
128- - To find icon SVG paths: browse heroicons.com or lucide.dev , copy the raw SVG code only — not the package import
129- - All inline SVGs: width 24, height 24, stroke="currentColor" or fill="#121F38 "
130- - Never use emojis as UI icons — always use inline SVGs
128+ - All icons use the ** shared icons file** at `src/components/ui/icons.tsx`
129+ - Only add icons to icons.tsx that are actually used — never pre-populate with unused icons
130+ - Import only what you need in each component — named imports only
131+ - SVG paths sourced from heroicons.com or lucide.dev — copy raw SVG markup only, no package imports
132+ - All SVGs: width and height appropriate to context, stroke="currentColor" or fill="#121F38 "
133+ - ** Never use emojis as UI icons** — always use inline SVGs from icons.tsx
131134
132135```tsx
133- // CORRECT — inline SVG
134- function TrophyIcon() {
135- return (
136- <svg width ="24" height ="24" viewBox ="0 0 24 24"
137- fill ="none " stroke ="currentColor " strokeWidth ="2">
138- <path d ="M 6 9H 4a 2 2 0 0 1- 2- 2V 5h 4"/>
139- <path d ="M 18 9h 2a 2 2 0 0 0 2- 2V 5h- 4"/>
140- <path d ="M 12 17v 4"/>
141- <path d ="M 8 21h 8"/>
142- <path d ="M 6 9a 6 6 0 0 0 12 0V 5H 6z "/>
143- </svg >
144- )
145- }
146-
147- // WRONG — never do this
136+ // CORRECT — named import from shared icons file
137+ import { TrophyIcon , ArrowRightIcon } from '@/components/ui/icons'
138+
139+ // WRONG — never install or import from icon libraries
148140import { Trophy } from 'lucide-react '
141+ import * as Icons from '@/components/ui/icons' // never import everything
149142```
150143
151144---
@@ -206,7 +199,8 @@ codeddevs-website/
206199│ │ │ ├── Badge.tsx
207200│ │ │ ├── Card.tsx
208201│ │ │ ├── Input .tsx
209- │ │ │ └── Textarea .tsx
202+ │ │ │ ├── Textarea .tsx
203+ │ │ │ └── icons.tsx # shared inline SVG icons
210204│ │ ├── sections/
211205│ │ │ ├── HeroSection.tsx
212206│ │ │ ├── ProductsSection.tsx
@@ -221,7 +215,7 @@ codeddevs-website/
221215│ │ │ └── ContactForm.tsx
222216│ │ └── admin/
223217│ │ ├── RichTextEditor.tsx
224- │ │ ├── ImageUpload.tsx
218+ │ │ ├── ImageUpload.tsx # includes react-image-crop
225219│ │ └── DataTable.tsx
226220│ ├── db/
227221│ │ ├── index.ts
@@ -281,7 +275,7 @@ Admin toggles this manually per post.
281275Controls the placement badge shown on the Recognition card.
282276Values: `'1st' | '2nd' | '3rd' | 'winner' | null`
283277Only relevant when show_in_recognition is true.
284- Displayed as an inline SVG icon + label — never as an emoji.
278+ Displayed as an inline SVG icon + label from icons .tsx — never as an emoji.
285279
286280### careers
287281```ts
@@ -409,13 +403,13 @@ Five sections in order:
409403- Fetches blog posts where `show_in_recognition = true` AND `is_published = true`
410404- Ordered by published_at DESC, limit 3
411405- Cards — text only, no cover image:
412- - Inline SVG placement icon + placement label (from ` placement` field)
406+ - Inline SVG placement icon (from icons.tsx) + placement label
413407 - Category badge
414408 - Blog post title (JetBrains Mono, bold)
415409 - Excerpt (IBM Plex Sans, 2 lines max, truncated)
416410 - Date (formatted, muted, small)
417411 - "Read the story →" → links to /blog/[slug]
418- - Placement display uses inline SVG icons — never emojis
412+ - Placement display uses inline SVG icons from icons.tsx — never emojis
419413- This is curated — admin manually toggles show_in_recognition per post
420414- If no recognition posts exist, section does not render
421415
@@ -500,6 +494,17 @@ By [author] · [formatted date] · [X min read]
500494- Blog covers: `codeddevs-website/blogs/` — 1200x630px
501495- Inline article images: 1200x800px
502496
497+ ### Image cropping (admin dashboard only)
498+ The `ImageUpload.tsx` component uses `react-image-crop` to let admins
499+ crop images before uploading to Cloudinary:
500+ - Team photo uploads → square crop locked (1:1 aspect ratio)
501+ - Blog cover uploads → landscape crop locked (1200:630 aspect ratio)
502+ - Product cover uploads → landscape crop locked (1200:630 aspect ratio)
503+ - Inline blog images → free crop (no aspect ratio lock)
504+
505+ `react-image-crop` is imported ONLY in `ImageUpload.tsx`.
506+ Never import it in any public page or component.
507+
503508### Cloudinary URL transformations
504509Use helpers from `src/lib/cloudinary.ts`. Never use raw Cloudinary URLs.
505510
@@ -575,7 +580,7 @@ const [products, posts] = await Promise.all([
5755806. Cloudinary for all content images — use transformation helpers
5765817. Resend for all email — never nodemailer or sendgrid
5775828. next/font/google for fonts — no CDN link tags
578- 9. No UI libraries — build from scratch with Tailwind
583+ 9. No UI component libraries on public pages — build from scratch with Tailwind. Exception: `react-image-crop` is permitted in `ImageUpload.tsx` (admin only) for image cropping before upload. Never import it in any public page or component.
57958410. cn() for all conditional classNames
58058511. No animations — nothing moves
58158612. Light theme only — no dark: variants
@@ -589,8 +594,8 @@ const [products, posts] = await Promise.all([
58959420. Blog URL /blog, displayed as "Updates" in all user-facing labels
59059521. Use borders sparingly — prefer spacing and background contrast
59159622. Design must feel human, not AI-generated — avoid generic layouts
592- 23. No emojis in UI components — use inline SVG icons only
593- 24. Never install icon libraries — copy raw SVG paths from heroicons.com or lucide.dev
597+ 23. No emojis in UI components — use inline SVG icons from icons.tsx only
598+ 24. Never install icon libraries — use shared icons.tsx with raw SVG paths sourced from heroicons.com or lucide.dev
594599
595600---
596601
@@ -623,4 +628,4 @@ const [products, posts] = await Promise.all([
623628
624629---
625630
626- *Last updated: May 2026*
631+ *Last updated: May 2026*
0 commit comments