π¨βπ» Source Code & More: https://www.codewithantonio.com/projects/duolingo-clone GitHub: https://github.com/AntonioErdeljac/next14-duolingo-clone
- Clerk: https://go.clerk.com/wmPbEeD
- Kenney Assets:https://kenney.nl/
- Freesound: https://freesound.org/
- Elevenlabs AI: https://elevenlabs.io/
- Flagpack: https://flagpack.xyz/
- μ΄ 11μκ°μ§λ¦¬ νν 리μΌμμλ λμ€λ§κ³ μ μ μ¬ν λλ§μ μΈμ΄ νμ΅ SaaSλ₯Ό λ§λλ λ°©λ²μ λ°°μλλ€.
- μ¬μ©μλ μΈμ΄ μ½μ€λ₯Ό μ νν μ μμΌλ©° μλ¦λ€μ΄ λμμΈ, μΊλ¦ν°, μ€λμ€ λ° μκ° ν¨κ³Όκ° ν¬ν¨λ κ°μ΄λ λ μ¨μ λ°μ μ μμ΅λλ€.
- Next.js 14, Drizzle ORM, PostgreSQL, μλ² μ‘μ , Stripe, ShadcnUI, Tailwind λ±μ λ°°μ°κ² λ©λλ€.
- π Next.js 14 & server actions
- π£ AI Voices using Elevenlabs AI
- π¨ Beautiful component system using Shadcn UI
- π Amazing characters thanks to KenneyNL
- π Auth using Clerk
- π Sound effects
- β€οΈ Hearts system
- π Points / XP system
- π No hearts left popup
- πͺ Exit confirmation popup
- π Practice old lessons to regain hearts
- π Leaderboard
- πΊ Quests milestones
- π Shop system to exchange points with hearts
- π³ Pro tier for unlimited hearts using Stripe
- π Landing page
- π Admin dashboard React Admin
- π§ ORM using DrizzleORM
- πΎ PostgresDB using NeonDB
- π Deployment on Vercel
- π± Mobile responsiveness
- shadcn/ui μ€μ
npx shadcn-ui@latest init
β Which style would you like to use? βΊ Default
β Which color would you like to use as base color? βΊ Slate
β Would you like to use CSS variables for colors? β¦ no / yes
- tailwind extention μ€μΉ
- shadcn-ui button μΆκ°
npx shadcn-ui@latest add button
- app/buttons/page.tsx μμ±
- Variants λ³ λ²νΌ νλ©΄
- components/ui/button.tsx μμ
- λ²νΌ variants, sizse μ 체 컀μ€ν°λ§μ΄μ§
- app/page.tsx -> app/(marketing)/page.tsx ν΄λ μ΄λ
- app/(marketing)/layout.tsx μμ±
- κ³΅ν΅ λ μ΄μμ μμ± (ν€λ, νΈν°)
- app/(marketing)/header.tsx μμ±
- ν€λ μ 보
- app/(marketing)/footer.tsx μμ±
- νΈν° μ 보
- Clerk μ΄ν리μΌμ΄μ μμ± λ° μ€μ
npm install @clerk/nextjs
.env
μμ±- clerk key μΆκ°
middleware.ts
μμ±- publicRoutes μΆκ°
app/layout.tsx
μμ - ClerkProvider μΆκ°
- svg μ΄λ―Έμ§ μΆκ°
public/hero.svg
public/mascot.svg
app/(marketing)/header.tsx
μμ - λ‘κ³ μΆκ°
- λ‘κ·ΈμΈ λ²νΌ μΆκ°
app/(marketing)/page.tsx
μμ - λ‘κ·ΈμΈ / λ―Έλ‘κ·ΈμΈ μνμ λ°λΌ UI λΆκΈ° μ²λ¦¬
- public κ΅κΈ° μ΄λ―Έμ§ μΆκ°
- app/(marketing)/footer.tsx μμ
- κ΅κ°λ³ κ΅κΈ° μΆκ°
app/(main)/layout.tsx
μμ±- λ©μΈ ν΄λ λ μ΄μμ μΆκ°
app/(main)/learn/page.tsx
μμ±- learn νμ΄μ§ μΆκ°
components/sidebar.tsx
μμ±- sidebarλ μ¬μ¬μ© κ°λ₯νλλ‘ λ§λ€κΈ° μν΄ componenets μ λ§λ¦
components/mobile-header.tsx
μμ±- lg μ΄νμμλ λͺ¨λ°μΌ ν€λκ° λ³΄μ΄λλ‘ κ΅μ²΄
components/mobile-sidebar.tsx
μμ±- λͺ¨λ°μΌ ν€λμμ 보μ¬μ€ mobile μ© μ¬μ΄λλ°
- Sheet λ‘ κ΅¬νν΄μ sidebar λ₯Ό μ¬νμ©ν΄μ νν
npx shadcn-ui@latest add sheet
- MobileHeader μμ μ¬μ©ν sheet
- public μ¬μ΄λλ° μ΄λ―Έμ§ μΆκ°
- components/sidebar-item.tsx μμ±
- μ¬μ΄λλ° λ©λ΄ μμ΄ν μ»΄ν¬λνΈ μΆκ°
- μμ΄μ½, λΌλ²¨, λ§ν¬ λ°μ΄ν°λ‘ λ©λ΄ λ§λ€κΈ°
- components/sidebar.tsx μμ
- μ¬μ΄λλ° μμ΄ν μ»΄ν¬λνΈ μ μ©
- app/(main)/layout.tsx μμ
- λ λ λ°°κ²½ μ κ±° λ° css μμ
- app/(main)/learn/page.tsx μμ
- StickyWrapper μΆκ°
- FeedWrapper μΆκ°
- components/sticky-wrapper.tsx μμ±
- StickyWrapper μ»΄ν¬λνΈ
- components/feed-wrapper.tsx μμ±
- FeedWrapper μ»΄ν¬λνΈ
- app/(main)/learn/header.tsx μμ±
- ν€λ μ»΄ν¬λνΈ μΆκ°
- λ€λ‘κ°κΈ° λ²νΌ, νμ΄ν μΆκ°
- components/user-progress.tsx μμ±
- μ€λ₯Έμͺ½μ κ³ μ λ μ μ μ§νμν© μ»΄ν¬λνΈ
- μ΄λ―Έμ§ μΆκ°
- public/heart.svg
- public/points.svg
- λ‘컬 mysql λ‘λ μ΄λ»κ² μ¬μ©νλμ§ κ²ν !
- λλ¦¬μ¦ μ€νλμ€λ λλ¦¬μ¦ κ΅¬μ± νμΌμ κ°μ Έμ λ°μ΄ν°λ² μ΄μ€μ μ°κ²°νκ³ κΈ°μ‘΄ λλ¦¬μ¦ SQL μ€ν€λ§λ₯Ό κΈ°λ°μΌλ‘ λͺ¨λ κ²μ νμ, μΆκ°, μμ λ° μ λ°μ΄νΈν μ μμ΅λλ€.
- λͺ μμ null λ° λΉ λ¬Έμμ΄ κ°, λΆμΈ, μ«μ λ° ν° μ μ, json κ°μ²΄ λ° json λ°°μ΄μ μ§μν©λλ€.
DrizzleKit Setting
- Dependencies
npm i drizzle-orm @neondatabase/serverless
npm i -D drizzle-kit
- package.json
- script μμ±
npm run db:studio
npm run db:push
- db/drizzle.ts μμ±
- λλΉ μ°κ²° μ 보
- db/schema.ts μμ±
- μ€ν€λ§ μ 보
npm i dotenv
- Dotenvλ .env νμΌμμ process.envλ‘ νκ²½ λ³μλ₯Ό λ‘λνλ μ λ‘ μ’ μμ± λͺ¨λμ λλ€.
- μ½λμ λ³λλ‘ νκ²½μ ꡬμ±μ μ μ₯νλ κ²μ 12μμ μ± λ°©λ²λ‘ μ κΈ°λ°μΌλ‘ ν©λλ€.
npm run db:push
npm i -D pg
npm run db:studio
mysql> status;
- mysql μ°κ²° μν μ 보
mysql> show databases;
- Database 리μ€νΈ
mysql> CREATE DATABASE somedatabase;
- λ°μ΄ν°λ² μ΄μ€ μμ±
mysql> select database();
- νμ¬ μ¬μ©μ€μΈ λ°μ΄ν° λ² μ΄μ€ μ‘°ν
mysql> use somedatabase;
Database changed
-
λ°μ΄ν°λ² μ΄μ€ μ¬μ©
db/queries.ts
μμ±- getCourses - μ½μ€ λ°μ΄ν° κ°μ Έμ€κΈ°
app/(main)/courses/page.tsx
μμ±- /courses νμ΄μ§ μμ±
- μΈμ΄ μ½μ€ μ ν
app/(main)/courses/list.tsx
μμ±- μ½μ€ μ ν 리μ€νΈ μ»΄ν¬λνΈ
app/(main)/courses/card.tsx
μμ±- μ½μ€ μ ν μμ΄ν μΉ΄λ μ»΄ν¬λνΈ
- κ΅κΈ°, μΈμ΄ μ΄λ¦ νμ
- db/schema.ts μμ
- userProgress ν μ΄λΈ μΆκ°
- courses, userProgress κ° relation μ°κ²°
- db/queries.ts μμ
- μ μ μ ν΄λΉνλ μ½μ€ μ§ν μν© κ°μ Έμ€κΈ°
- μ½μ€ μ 보 κ°μ Έμ€κΈ°
- app/(main)/courses/page.tsx μμ
- UserProgress κ°μ Έμμ μ½μ€ νμ
- app/(main)/learn/page.tsx μμ
- UserProgress μ 보λ₯Ό κ°μ Έμμ μ§νμ€μΈ μ½μ€κ° μμΌλ©΄ 리λλ νΈ μ²λ¦¬
- loading.tsx μμ±
- Page root μ loading μ μΆκ°νλ©΄ μλμΌλ‘ νμλ¨
- actions/user-progress.ts μμ±
- User μ§ν μν©μ λ°μ΄ν°λ² μ΄μ€μ μ λ°μ΄νΈ ν μ‘μ
- app/(main)/courses/list.tsx μμ
- μ½μ€ ν΄λ¦μ μ½μ€ μ 보 μ μ₯ λ° Learn νμ΄μ§ 리λλ νΈ
- μΉ΄λ ν΄λ¦ μ°κ²°
- app/layout.tsx μμ
- Body μμ Toaster μΆκ°
npx shadcn-ui@latest add sonner
- ν μ€ν°
- upsert: UPDATE + INSERT ν©μ±μ΄
- scripts/seed.ts μμ±
- λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν λ° μΈμ΄ νμ΅ μ 보 κΈ°μ΄ μμ±
- package.json μμ
- seed.ts μ€ν μ€ν¬λ¦½νΈ μΆκ°
- tsx λΌμ΄λΈλ¬λ¦¬ μ¬μ©
- app/(main)/learn/page.tsx μμ
- μ νλ μΈμ΄ μ½μ€λ‘ ν€λ νμ΄ν λ³κ²½
npm i -D tsx
- νμ μ€ν¬λ¦½νΈ μ€ν
- tsx λμμΌλ‘ bun μ¬μ© κ°λ₯
- BunμΌλ‘ JavaScript λ° TypeScript νλ‘μ νΈλ₯Ό κ°λ°, ν μ€νΈ, μ€ν, λ²λ€λ§
- Bunμ λ²λ€λ¬, ν μ€νΈ λ°μ², Node.js νΈν ν¨ν€μ§ κ΄λ¦¬μκ° ν¬ν¨λ μλλ₯Ό μν΄ μ€κ³λ μ¬μΈμ JavaScript λ°νμ λ° ν΄ν·
- db/schema.ts μμ
- table, relations μμ±
- units
- lessons
- challenges
- challenge_options
- challenge_progress
- table, relations μμ±
- scripts/seed.ts μμ
- unit, lessons, challenge, challengeOptions μ΄κΈ° λ°μ΄ν°λ² μ΄μ€ μΆκ°
- db/queries.ts μμ
- getUnits μμ±
- app/(main)/learn/page.tsx μμ
- unit μ 보 Json μΌλ‘ μμ νμ
- db/queries.ts μμ
- getUnit - userId λΉκ΅ μΆκ°
- app/(main)/learn/page.tsx μμ
- Unit μΆκ°
- app/(main)/learn/unit.tsx μμ±
- Unit μ»΄ν¬λνΈ
- app/(main)/learn/unit-banner.tsx μμ±
- μ½μ€ κ²½λ‘μμ 보μ¬μ§λ λ μ¨ μ λ λ°°λ
- app/(main)/learn/lesson-button.tsx μμ±
- μ½μ€ κ²½λ‘μμ 보μ¬μ§λ λ μ¨ λ²νΌ
- react-circular-progressbar λ₯Ό μ¬μ©ν΄μ λ²νΌμ μ§νμν© νμ
- npm i react-circular-progressbar
- SVGλ‘ μ μλκ³ κ΄λ²μνκ² μ¬μ©μ μ μν μ μλ μν μ§νλ₯ νμμ€ μ»΄ν¬λνΈμ λλ€.
- db/queries.ts μμ
- getCourseProgress: μ§νμ€μΈ μ½μ€ κ°μ Έμ€κΈ°
- getLesson: λ μ¨ μ 보 κ°μ Έμ€κΈ°
- getLessonPercentage: λ μ¨ μ§νλ₯ κ°μ Έμ€κΈ°
- app/(main)/learn/page.tsx μμ
- activeLesson λλΉ λ°μ΄ν° μΆκ°
- activeLessonPercentage λλΉ λ°μ΄ν° μΆκ°
- db/queries.ts μμ
- normalizedData μ±λ¦°μ§κ° μμλ μμΈ μ²λ¦¬ μΆκ°
- app/lesson/layout.tsx μμ±
- lesson κΈ°λ³Έ λ μ΄μμ
- app/lesson/page.tsx μμ±
- λ μ¨ νμ΄μ§
- app/lesson/quiz.tsx μμ±
- ν΄μ¦ μ»΄ν¬λνΈ μΆκ°
- app/lesson/header.tsx μμ±
- ν€λ μ»΄ν¬λνΈ μΆκ°
- shadcn-ui progress μ»΄ν¬λνΈ μΆκ°
- npx shadcn-ui@latest add progress
- dialog, zustand λνλμ μΆκ°
- store/use-exit-modal.ts μμ±
- exit modal μνκ΄λ¦¬
- app/layout.tsx μμ
- μ μ ExitModal μΆκ°
- app/lesson/header.tsx μμ
- X λ²νΌ ν΄λ¦ μ°κ²°
- components/modals/exit-modal.tsx μΆκ°
- μ’ λ£ νμ μΆκ°
- npx shadcn-ui@latest add dialog
- npm i zustand
- μκ³ λΉ λ₯΄λ©° νμ₯ κ°λ₯ν λ² μ΄λ³Έ μν κ΄λ¦¬ μ루μ μ λλ€.
- Zustandλ ν μ κΈ°λ°μΌλ‘ νλ νΈμν APIλ₯Ό μ 곡ν©λλ€
- μ’λΉ μμ λ¬Έμ , React λμμ±, νΌν© λ λλ¬ κ°μ 컨ν μ€νΈ μμ€κ³Ό κ°μ μΌλ°μ μΈ ν¨μ μ μ²λ¦¬
- app/lesson/quiz.tsx μμ
- app/lesson/question-bubble.tsx μμ±
- μ§λ¬Έ λ²λΈ μ»΄ν¬λνΈ
- app/lesson/challenge.tsx μμ±
- μ§λ¬Έ μ»΄ν¬λνΈ
- app/lesson/card.tsx μμ±
- μ λ΅ μΉ΄λ μ»΄ν¬λνΈ
- correct, wrong μ λ°λΌ μΉ΄λ μν λ³κ²½
- Elevenlabs AI λ‘ μ리 λ§λ€κΈ°
- app/lesson/card.tsx μμ
- 보기 ν΄λ¦μ μμ± μ¬μ μΆκ°
- react-use μ useAudio μ¬μ©νκΈ°
- app/lesson/quiz.tsx μμ
- Footer μΆκ°
- app/lesson/footer.tsx μμ±
- Footer μ»΄ν¬λνΈ
- status μ λ°λΌ μν λ³κ²½
- es_man.mp3
- es_woman.mp3
- es_robot.mp3
- npm i react-use
- app/lesson/quiz.tsx μμ
- footer check λ²νΌ μ΅μ
μ νμ λ‘μ§ μΆκ°
- μ±κ³΅μ λ€μ λ¬Έμ μ§ν
- μ€ν¨μ ννΈ κ°μ λ° λ€μμλ μ²λ¦¬
- footer check λ²νΌ μ΅μ
μ νμ λ‘μ§ μΆκ°
- actions/challenge-progress.ts μμ±
- λ¬Έμ μ λ΅ μ μ§ν μν© μ λ°μ΄νΈ
- scripts/seed.ts μμ
- λ¬Έμ μΆκ°
- actions/user-progress.ts μμ
- λλΉμ ννΈ κ°μμν€λ λ‘μ§ μΆκ°
- μ λ΅, μ€λ΅, μλ£ μμ± νμΌ μΆκ°
- scripts/seed.ts μμ
- κΈ°λ³Έ μ±λ¦°μ§ μΆκ°
- app/lesson/quiz.tsx μμ
- μ±λ¦°μ§ μλ£ νμ
- 보기 μ νμ μ€λμ€ μ¬μ
- μ±λ¦°μ§ μλ£ μ€λμ€ μλμ¬μ
- confetti ν¨κ³Ό μΆκ°
- app/lesson/result-card.tsx μμ±
- μ±λ¦°μ§ μλ£ κ²°κ³Ό μΉ΄λ μ»΄ν¬λνΈ
- correct.wav
- incorrect.wav
- finish.mp3
- finish.svg
- npm i react-confetti
- μ’ μ΄ κ½κ°λ£¨ μ λλ©μ΄μ
- store/use-hearts-modal.ts μμ±
- ννΈλͺ¨λ¬μμ μ¬μ©ν store
- components/modals/hearts-modal.tsx μμ±
- ννΈλͺ¨λ¬ μ»΄ν¬λνΈ
- app/layout.tsx μμ
- HeartsModal μΆκ°
- app/lesson/quiz.tsx μμ
- μ λ΅/μ€λ΅ μ νμ Heart κ° μμΌλ©΄ λͺ¨λ¬ νμ νμ
- mascot_bad.svg
- app/(main)/shop/page.tsx μμ±
- μμ νμ΄μ§
- app/(main)/shop/items.tsx μμ±
- μμ μ μμ΄ν μ νμνλ μ»΄ν¬λνΈ
- actions/user-progress.ts μμ
- refillHearts μΆκ°
- point μ°¨κ° ν ννΈ ν 리ν
- refillHearts μΆκ°
- db/schema.ts μμ
- userSubscription ν μ΄λΈ μμ±
- stripe μ€μΉ
- lib/stripe.ts μμ±
- Stripe κ°μ²΄ μμ±
- API λ²μ , Stripe secret ν€ μΆκ°
- Stripe κ°μ²΄ μμ±
- .env μμ
- Stripe λμ보λ account μμ±
- STRIPE_API_KEY μΆκ°
- 리μμ€ μΆκ°
- public/unlimited.svg
- actions/user-subscription.ts μμ±
- ꡬλ μ 보 λ° callback url μ§μ
- stripe μΈμ
μμ±
- κ²°μ λ°μ΄ν°
- λ©ν λ°μ΄ν°
- db/queries.ts μμ
- νμ¬ κ΅¬λ μ€μΈμ§ μ 보 λ°ν
- lib/utils.ts μμ
- μ λ κ²½λ‘ μμ± μ νΈ μΆκ°
- app/(main)/shop/page.tsx μμ
- νλ‘ λ²μ μ¬μ© μ 무μ λ°λΌ μ»΄ν¬λνΈ μν λ³κ²½
- app/(main)/shop/items.tsx μμ
- onUpgrade νλ‘ λ²μ μ ν
- Stripe Webhooks μ€μ
- app/api/webhooks/stripe/route.ts μμ±
- stripe κ²°μ κ° 200μΌλ‘ μ±κ³΅νλ©΄ DB
userSubscription
ν μ΄λΈμ ꡬλ μ 보 λ° λ§λ£ μΌμ μΆκ° - checkout, invoice μ λ°λΌ insert, update λ‘ μ²λ¦¬
- stripe κ²°μ κ° 200μΌλ‘ μ±κ³΅νλ©΄ DB
- .env - STRIPE_WEBHOOK_SECRET μΆκ°
- middleware.ts μμ
export default authMiddleware({ publicRoutes: ["/", "/api/webhooks/stripe"], });
- publicRoutes webhooks api μΆκ°
- app/api/webhooks/stripe/route.ts μμ±
Stripe Webhooks μ€μ
- stripe λ‘κ·ΈμΈ
stripe login
Your pairing code is: lively-merit-rosy-serene
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser or visit https://dashboard.stripe.com/stripecli/confirm_auth?t=some_key
- λ§ν¬λ‘ μ΄λ
- Stripe CLIκ° κ³μ μ 보μ μ‘μΈμ€ν μ μλλ‘ νμ©νμκ² μ΅λκΉ? Allow access ν΄λ¦
- Access granted - CLI λ‘ λμκ°κΈ°
- stripe listen forward κ²½λ‘ μ€μ
stripe listen --forward-to localhost:3000/api/webhooks/stripe
A newer version of the Stripe CLI is available, please update to: v1.19.4
> Ready! You are using Stripe API Version [2024-04-10]. Your webhook signing secret is your_secret (^C to quit)
- forward-to κ²½λ‘λ₯Ό μ€μ webhooks μ²λ¦¬ν κ³³μΌλ‘ λ³κ²½
- your_secret λΆλΆμ΄ μ¬μ©ν secret key
- stripe μ΄λ²€νΈ νΈλ¦¬κ±° μ΄ν -> http status code 200 νμΈ
- stripe CLIλ‘ μ΄λ²€νΈ νΈλ¦¬κ±°
stripe trigger payment_intent.succeeded
- Settings > Billing > Customer portal - ν μ€νΈ λ§ν¬ νμ±ν Launch customer portal with a link
- Activate test link
- νμ±ν νλ©΄ μ΄λ―Έ κ²°μ ν κ²°μ λ²νΌ ν΄λ¦μ κ³ κ° κ²°μ μ 보 ν¬νΈλ‘ μ΄λ
- Stripe - Error codes api_key_expired
- The API key provided has expired. Obtain your current API keys from the Dashboard and update your integration to use them.
- Restricted keys λ₯Ό μλ‘ Roll key ν΄μ μ¬μμ±
stripe login
λΆν° λ€μ CLI λͺ λ Ήμ΄ μ€ν νλ©΄ μλ£
as-is
import "dotenv/config";
import type { Config } from "drizzle-kit";
export default {
schema: "./db/schema.ts",
out: "./drizzle",
driver: "pg",
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;
to-be
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: "postgresql",
schema: "./db/schema.ts",
out: "./drizzle",
dbCredentials: {
url: process.env.DATABASE_URL!,
}
})
μΉν listen μ μ€ννκ³ κ²°μ ν μ€νΈλ₯Ό ν΄μΌ μΉν μ΄ μ μμ μΌλ‘ μλ!
stripe listen --forward-to localhost:3000/api/webhooks/stripe
- npm i stripe
- app/(main)/learn/page.tsx μμ
- μλ¨ μ§ν μν© ν€λ userSubscription μν λ°μ
- app/lesson/page.tsx μμ
- ν΄μ¦ μ»΄ν¬λνΈμ userSubscription μν μ λ¬
- app/lesson/quiz.tsx μμ
- Props userSubscription type λ³κ²½
- app/lesson/header.tsx μμ
- infinite μμ΄μ½ shirink-0 μ μ©
- actions/user-progress.ts μμ
- 리ν μΉ΄μ΄νΈ μμ μ΄λ
- μμΈμ²λ¦¬ νμ±ν
- db/queries.ts μμ
- getUnits μ λ ¬ μ μ©
- constants.ts μμ
- κ³΅ν΅ μμ κ΄λ¦¬
- app/(main)/leaderboard/page.tsx μμ±
- 리λ보λ νμ΄μ§ μΆκ°
- Separator, Avatar - shadcnui μΆκ°
- 리λ보λ λνΉ λ·° μΆκ°
- db/queries.ts μμ
- μμ μ μ 10λͺ μ κ°μ Έμ€λ getTopTenUsers μΆκ°
- app/(main)/quests/page.tsx μμ±
- νμ€νΈ νμ΄μ§ μΆκ°
- νμ€νΈ νλͺ© μΆκ°
Propmo / Qeust
- components/promo.tsx μμ±
- νλ‘λͺ¨μ μ»΄ν¬λνΈ
- components/quests.tsx μμ±
- νμ€νΈ μ»΄ν¬λνΈ
- μ¬μ΄λλ° λ©λ΄μ νλ‘λͺ¨μ
/νμ€νΈ μ»΄ν¬λνΈ μΆκ°
- app/(main)/learn/page.tsx
- app/(main)/shop/page.tsx
- app/(main)/leaderboard/page.tsx
- app/(main)/quests/page.tsx
- flex νλͺ© μΆμ λ°©μμ μ μ΄νκΈ° μν μ νΈλ¦¬ν°
- flex νλͺ©μ΄ μΆμλμ§ μλλ‘ νλ €λ©΄
shrink-0
μ μ¬μ©
- npx shadcn-ui@latest add avatar
- npx shadcn-ui@latest add separator
- dependencies μΆκ°
- npm i react-admin ra-data-simple-rest
- app/admin/page.tsx μμ±
- Admin Page
- app/admin/app.tsx μμ±
- React Admin Page
- Courses 리μμ€ μΆκ°
- app/api/courses/route.ts μμ±
- GET λ©μλ μμ±
- Admin Resource λ‘ μ¬μ© ν Courses λ₯Ό λλΉμμ μ‘°ν ν μ 곡
- next.config.mjs μμ
- source / headers μΆκ°
- lib/admin.ts μμ±
- adminIds λ°°μ΄μ μ΄λλ―Ό κ΄λ¦¬μ userId λ₯Ό λ£κ³ λΉκ΅ν΄μ admin μ 무 νλ¨
- app/admin/page.tsx μμ
- admin μ΄ μλλ©΄ redirect
- app/api/courses/route.ts μμ
- admin μ΄ μλλ©΄ 401 μλ¬ λ°ν
-
app/admin/course/list.tsx μμ
- 컀μ€ν 리μ€νΈ μΆκ°
- ReactAdmin μ»΄ν¬λνΈ μ¬μ©
-
app/admin/app.tsx μμ
- list, create, edit(delete) μΆκ°
-
app/api/courses/route.ts μμ
- POST λ©μλ μΆκ°
-
app/admin/course/create.tsx μμ±
- λλΉ λ°μ΄ν° μμ±
- POST λ‘ λ°μ΄ν° μμ±
- ReactAdmin μ»΄ν¬λνΈ μ¬μ©
-
scripts/reset.ts μΆκ°
- λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μ€ν¬λ¦½νΈ
-
app/admin/course/edit.tsx μμ±
- λλΉ λ°μ΄ν° μμ , μμ μΆκ°
- ReactAdmin μ»΄ν¬λνΈ μ¬μ©
-
app/api/courses/[courseId]/route.ts μμ±
- Edit μ νμν API μμ±
- GET, PUT, DELETE λ‘ μμ μ§μ
- app/admin/unit μμ±
- unit κ΄λ ¨ admin νμ΄μ§ μμ±
- create, edit, list μμ±
- app/api/units μμ±
- unit κ΄λ ¨ API μμ±
- app/admin/lesson μμ±
- lesson κ΄λ ¨ admin νμ΄μ§ μμ±
- create, edit, list μμ±
- app/api/lessons μμ±
- lesson κ΄λ ¨ API μμ±
- app/admin/challenge μμ±
- challenge κ΄λ ¨ admin νμ΄μ§ μμ±
- create, edit, list μμ±
- app/api/challenges μμ±
- challenge κ΄λ ¨ API μμ±
- app/admin/challengeOption μμ±
- challengeOption κ΄λ ¨ admin νμ΄μ§ μμ±
- create, edit, list μμ±
- app/api/challengeOptions μμ±
- challengeOption κ΄λ ¨ API μμ±
ES6, React λ° λ¨Έν°λ¦¬μΌ λμμΈμ μ¬μ©νμ¬ REST/GraphQL APIλ₯Ό κΈ°λ°μΌλ‘ λΈλΌμ°μ μμ μ€νλλ λ°μ΄ν° κΈ°λ° μ ν리μΌμ΄μ μ ꡬμΆνκΈ° μν νλ‘ νΈμλ νλ μμν¬μ λλ€. μ΄μ μλ admin-on-restλ‘ λͺ λͺ λμμ΅λλ€. μ€ν μμ€μ΄λ©° marmelabμμ μ μ§ κ΄λ¦¬ν©λλ€.
- npm i react-admin
REST/GraphQL μλΉμ€ μμ κ΄λ¦¬μ μ ν리μΌμ΄μ μ ꡬμΆνκΈ° μν νλ‘ νΈμλ νλ μμν¬μΈ react-adminμ μν κ°λ¨ν REST λ°μ΄ν° 곡κΈμμ λλ€.
- npm i ra-data-simple-rest
- next/dynamic μΌλ‘ client side rendering λ§λ€κΈ°
- scripts/prod.ts μμ±
- package.json μμ
- prod μ© seed μΆκ°