├── public ├── _ads.txt ├── logo.png ├── favicon.ico ├── images │ ├── trophy.png │ ├── favicon.ico │ ├── calculator-og.jpg │ ├── edwardsoyfit.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── coachs │ │ ├── mathias.png │ │ └── mathias2.jpg │ ├── default-workout.jpg │ ├── equipment │ │ ├── band.png │ │ ├── bench.png │ │ ├── plate.png │ │ ├── barbell.png │ │ ├── dumbbell.png │ │ ├── bodyweight.png │ │ ├── kettlebell.png │ │ └── pull-up-bar.png │ ├── nutripure-logo.webp │ ├── nutripure-gellules.png │ ├── default-og-image_en.jpg │ ├── default-og-image_es.jpg │ ├── default-og-image_fr.jpg │ ├── default-og-image_pt.jpg │ ├── default-og-image_ru.jpg │ ├── default-og-image_zh.jpg │ ├── emojis │ │ ├── WorkoutCoolCry.png │ │ ├── WorkoutCoolLoL.png │ │ ├── WorkoutCoolChief.png │ │ ├── WorkoutCoolHappy.png │ │ ├── WorkoutCoolHuuuu.png │ │ ├── WorkoutCoolLove.png │ │ ├── WorkoutCoolMeeeh.png │ │ ├── WorkoutCoolRich.png │ │ ├── WorkoutCoolSwag.png │ │ ├── WorkoutCoolTong.png │ │ ├── WorkoutCoolWooow.png │ │ ├── WorkoutCoolBiceps.png │ │ ├── WorkoutCoolMedical.png │ │ ├── WorkoutCoolPolice.png │ │ ├── WorkoutCoolShoked.png │ │ ├── WorkoutCoolShoked2.png │ │ ├── WorkoutCoolTeeths.png │ │ ├── WorkoutCoolYeahOk.png │ │ ├── WorkoutCoolhaphap.png │ │ ├── WorkoutCoolEmbarassed.png │ │ ├── WorkoutCoolExhausted.png │ │ ├── WorkoutCoolKittyGirl.png │ │ └── WorkoutCoolDisapointed.png │ ├── nutripure-gellules-2.png │ ├── placeholders │ │ ├── no-image.jpg │ │ └── coach-avatar.png │ ├── old-nutripure-gellules.png │ └── screenshots │ │ └── heart-rate-zones │ │ ├── en.jpg │ │ ├── es.jpg │ │ ├── fr.jpg │ │ ├── og.jpg │ │ ├── pt.jpg │ │ ├── ru.jpg │ │ └── zh-CN.jpg ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── manifest.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── src ├── shared │ ├── constants │ │ ├── regexs.ts │ │ ├── success.ts │ │ ├── cookies.ts │ │ ├── screen.ts │ │ ├── placeholders.ts │ │ ├── workout-set-types.ts │ │ ├── paths.ts │ │ ├── errors.ts │ │ └── statistics.ts │ ├── lib │ │ ├── version.ts │ │ ├── format.ts │ │ ├── guards.ts │ │ ├── utils.ts │ │ ├── logger.ts │ │ ├── revenuecat │ │ │ └── index.ts │ │ ├── prisma.ts │ │ ├── location │ │ │ ├── location.ts │ │ │ └── eu-countries.ts │ │ ├── mdx │ │ │ └── load-mdx.ts │ │ ├── network │ │ │ └── use-network-status.ts │ │ ├── server-url.ts │ │ ├── workout-session │ │ │ ├── workout-session.api.ts │ │ │ ├── types │ │ │ │ └── workout-session.ts │ │ │ └── equipments.ts │ │ ├── analytics │ │ │ ├── events.ts │ │ │ └── client.tsx │ │ ├── premium │ │ │ ├── use-premium-redirect.ts │ │ │ └── providers │ │ │ │ └── base-provider.ts │ │ ├── mail │ │ │ └── sendEmail.ts │ │ └── i18n-mapper.ts │ ├── schemas │ │ └── url.ts │ ├── types │ │ └── storage.ts │ ├── styles │ │ └── additional-styles │ │ │ └── utility-patterns.css │ ├── hooks │ │ ├── useBoolean.ts │ │ ├── useIsMobile.ts │ │ └── use-premium-plans.ts │ ├── api │ │ └── safe-actions.ts │ └── config │ │ └── site-config.ts ├── features │ ├── auth │ │ ├── verify-email │ │ │ └── constants.ts │ │ ├── signin │ │ │ ├── schema │ │ │ │ └── signin.schema.ts │ │ │ └── model │ │ │ │ └── useSignIn.ts │ │ ├── forgot-password │ │ │ ├── forgot-password.schema.ts │ │ │ └── model │ │ │ │ └── useForgotPassword.tsx │ │ ├── signup │ │ │ └── schema │ │ │ │ └── signup.schema.ts │ │ ├── ui │ │ │ ├── SignUpButton.tsx │ │ │ ├── SignInButton.tsx │ │ │ ├── AuthButtonServer.tsx │ │ │ └── LoggedInButton.tsx │ │ ├── model │ │ │ └── useLogout.ts │ │ ├── reset-password │ │ │ └── schema │ │ │ │ └── reset-password.schema.ts │ │ └── lib │ │ │ └── auth-client.ts │ ├── consent-banner │ │ ├── schema │ │ │ └── tracking-consent.schema.ts │ │ └── model │ │ │ └── tracking-consent.action.ts │ ├── release-notes │ │ ├── ui │ │ │ └── index.ts │ │ ├── hooks │ │ │ └── index.ts │ │ └── index.ts │ ├── update-password │ │ ├── lib │ │ │ ├── validate-password.ts │ │ │ └── hash.ts │ │ └── model │ │ │ └── update-password.schema.ts │ ├── email │ │ ├── email.schema.ts │ │ └── email.action.ts │ ├── workout-session │ │ ├── model │ │ │ ├── use-workout-session.ts │ │ │ └── use-workout-sessions.ts │ │ ├── hooks │ │ │ └── use-donation-modal.ts │ │ ├── lib │ │ │ └── workout-set-labels.ts │ │ ├── ui │ │ │ └── workout-sessions-synchronizer.tsx │ │ ├── types │ │ │ └── workout-set.ts │ │ └── actions │ │ │ └── delete-workout-session.action.ts │ ├── leaderboard │ │ ├── models │ │ │ └── types.ts │ │ ├── ui │ │ │ └── leaderboard-skeleton.tsx │ │ ├── hooks │ │ │ ├── use-top-workout-users.ts │ │ │ └── use-user-position.ts │ │ └── lib │ │ │ └── utils.ts │ ├── contact │ │ └── support │ │ │ ├── contact-support.schema.ts │ │ │ └── contact-support.action.ts │ ├── ads │ │ └── hooks │ │ │ └── useUserSubscription.ts │ ├── contact-feedback │ │ ├── model │ │ │ ├── contact-feedback.schema.ts │ │ │ └── contact-feedback.action.ts │ │ └── ui │ │ │ └── ReviewInput.tsx │ ├── theme │ │ ├── ThemeProviders.tsx │ │ ├── ui │ │ │ └── ThemeSynchronizer.tsx │ │ └── ThemeToggle.tsx │ ├── statistics │ │ ├── components │ │ │ └── index.ts │ │ ├── types │ │ │ └── index.ts │ │ └── hooks │ │ │ └── use-chart-theme.ts │ ├── layout │ │ ├── model │ │ │ └── use-sidebar.store.tsx │ │ ├── page-heading.tsx │ │ └── useSidebarToggle.ts │ ├── workout-builder │ │ ├── index.ts │ │ ├── hooks │ │ │ ├── use-workout-session.ts │ │ │ └── use-exercises.ts │ │ ├── schema │ │ │ └── get-exercises.schema.ts │ │ ├── model │ │ │ └── favorite-exercises-synchronizer.tsx │ │ ├── ui │ │ │ ├── muscles.module.css │ │ │ └── favorite-button.tsx │ │ ├── actions │ │ │ ├── pick-exercise.action.ts │ │ │ ├── get-favorite-exercises.action.ts │ │ │ └── sync-favorite-exercises.action.ts │ │ └── types │ │ │ └── index.ts │ ├── premium │ │ └── ui │ │ │ └── index.ts │ ├── admin │ │ └── programs │ │ │ ├── ui │ │ │ └── create-program-button.tsx │ │ │ └── actions │ │ │ └── delete-program.action.ts │ ├── form │ │ └── SubmitButton.tsx │ └── page │ │ └── layout.tsx ├── index.d.ts ├── components │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── loader.tsx │ │ ├── theme-provider.tsx │ │ ├── collapsible.tsx │ │ ├── title-with-dot.tsx │ │ ├── skeleton.tsx │ │ ├── divider.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── star-button.tsx │ │ ├── textarea.tsx │ │ ├── phone-frame-preview.tsx │ │ ├── toaster.tsx │ │ ├── sonner.tsx │ │ ├── Bento.tsx │ │ ├── local-alert.tsx │ │ ├── ToastSonner.tsx │ │ ├── link.tsx │ │ └── hover-card.tsx │ ├── version.tsx │ ├── ads │ │ ├── HorizontalTopBanner.tsx │ │ ├── HorizontalBottomBanner.tsx │ │ ├── index.ts │ │ ├── AdPlaceholder.tsx │ │ ├── VerticalLeftBanner.tsx │ │ ├── VerticalRightBanner.tsx │ │ ├── AdWrapper.tsx │ │ ├── InArticle.tsx │ │ ├── EzoicAd.tsx │ │ └── VerticalAdBanner.tsx │ ├── svg │ │ ├── Youtube.tsx │ │ ├── Calendly.tsx │ │ ├── IconCheckboxCheck.tsx │ │ ├── UnderlineSvg.tsx │ │ ├── DotPattern.tsx │ │ ├── VerifiedBadge.tsx │ │ ├── LogoSvg.tsx │ │ ├── GoogleSvg.tsx │ │ └── DiscordSvg.tsx │ ├── pwa │ │ └── ServiceWorkerRegistration.tsx │ ├── utils │ │ └── TailwindIndicator.tsx │ └── premium │ │ └── RemoveAdsText.tsx └── entities │ ├── user │ ├── schemas │ │ ├── get-user.schema.ts │ │ └── update-user.schema.ts │ ├── model │ │ ├── useCurrentUser.ts │ │ ├── useCurrentSession.ts │ │ ├── get-server-session-user.ts │ │ ├── update-user-locale.ts │ │ ├── use-auto-locale.ts │ │ └── update-user.action.ts │ ├── types │ │ └── session-user.ts │ └── lib │ │ └── display-name.ts │ ├── program-session │ └── types │ │ └── program-session.types.ts │ └── exercise │ ├── shared │ └── muscles.tsx │ └── types │ └── exercise.types.ts ├── .npmrc ├── app ├── [locale] │ ├── (app) │ │ ├── auth │ │ │ ├── signout │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── (auth-layout) │ │ │ │ ├── signin │ │ │ │ │ └── page.tsx │ │ │ │ ├── reset-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── forgot-password │ │ │ │ │ └── page.tsx │ │ │ │ └── signup │ │ │ │ │ └── page.tsx │ │ │ ├── verify-email │ │ │ │ ├── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── error.tsx │ │ │ ├── error │ │ │ │ └── page.tsx │ │ │ └── verify-request │ │ │ │ └── page.tsx │ │ ├── onboarding │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── [slug] │ │ │ └── layout.tsx │ │ ├── tools │ │ │ ├── calorie-calculator │ │ │ │ └── shared │ │ │ │ │ ├── types │ │ │ │ │ └── index.ts │ │ │ │ │ └── components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── InfoButton.tsx │ │ │ └── heart-rate-zones │ │ │ │ ├── ui │ │ │ │ └── components │ │ │ │ │ └── ScrollToTopButton.tsx │ │ │ │ └── seo │ │ │ │ └── page-content.ts │ │ ├── about │ │ │ └── page.tsx │ │ ├── (legal-and-payment) │ │ │ ├── layout.tsx │ │ │ └── legal │ │ │ │ ├── privacy │ │ │ │ └── page.tsx │ │ │ │ ├── terms │ │ │ │ └── page.tsx │ │ │ │ └── sales-terms │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── programs │ │ │ └── page.tsx │ │ ├── leaderboard │ │ │ └── page.tsx │ │ └── premium │ │ │ └── page.tsx │ ├── not-found.tsx │ ├── (admin) │ │ └── admin │ │ │ ├── not-found.tsx │ │ │ ├── [...catchAll] │ │ │ ├── page.tsx │ │ │ └── not-found.tsx │ │ │ ├── settings │ │ │ └── page.tsx │ │ │ ├── programs │ │ │ ├── [id] │ │ │ │ └── edit │ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ │ └── layout.tsx │ └── @modal │ │ └── (.)auth │ │ └── login │ │ └── page.tsx ├── api │ ├── auth │ │ └── [...all] │ │ │ └── route.ts │ ├── programs │ │ ├── route.ts │ │ └── [slug] │ │ │ ├── route.ts │ │ │ ├── progress │ │ │ └── route.ts │ │ │ └── sessions │ │ │ └── [sessionSlug] │ │ │ └── route.ts │ ├── webhooks │ │ └── stripe │ │ │ └── route.ts │ ├── premium │ │ ├── checkout │ │ │ └── route.ts │ │ └── billing-portal │ │ │ └── route.ts │ └── revenuecat │ │ └── link-user │ │ └── route.ts └── robots.txt ├── locales ├── types.ts └── server.ts ├── .prettierrc ├── .vscode └── settings.json ├── prisma └── migrations_backup │ ├── 20250626205904_remove_payment_table_keep_ispremium │ └── migration.sql │ ├── migration_lock.toml │ ├── 20250708214116_add_rating_to_wkt_sessions │ └── migration.sql │ ├── 20250414232816_add_first_name_and_last_name │ └── migration.sql │ ├── 20250615160343_add_muscle_to_a_workout_session │ └── migration.sql │ ├── 20250625195907_add_program_visibility │ └── migration.sql │ ├── 20250626134345_remove_emoji_on_program │ └── migration.sql │ ├── 20240726_simplify_subscription_model │ └── migration.sql │ ├── 20250623143952_remove_webhook_events │ └── migration.sql │ ├── 20250613095031_add_multi_column_support │ └── migration.sql │ ├── 20250626204136_remove_payment_table │ └── migration.sql │ ├── 20250505114841_add_user_role │ └── migration.sql │ ├── 20250626205121_remove_legacy_premium_fields │ └── migration.sql │ ├── 20250414170807_add_feedbacks │ └── migration.sql │ ├── 20250614153656_remove_value_int_value_sec_unit_from_workoutset │ └── migration.sql │ ├── 20250505191954_admin_and_user_lowercase │ └── migration.sql │ ├── 20250615170916_add_cascade_delete_workout_sessions │ └── migration.sql │ ├── 20250414174246_rename_feedbacks │ └── migration.sql │ ├── 20250707114920_add_user_favorite_exercises │ └── migration.sql │ ├── 20250117000000_add_statistics_indexes │ └── migration.sql │ └── 20250623144324_add_webhook_events │ └── migration.sql ├── postcss.config.mjs ├── workout-cool.code-workspace ├── nextauth.d.ts ├── components.json ├── next.config.ts ├── docker-compose.yml ├── scripts └── setup.sh ├── .gitignore ├── tsconfig.json ├── Dockerfile ├── Makefile ├── LICENSE ├── content └── about │ └── zh-CN.mdx ├── CONTRIBUTING.md └── emails └── DeleteAccountEmail.tsx /public/_ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-3437447245301146, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: snouzy 2 | ko_fi: workoutcool 3 | # buy_me_a_coffee: workout_cool 4 | -------------------------------------------------------------------------------- /src/shared/constants/regexs.ts: -------------------------------------------------------------------------------- 1 | export const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d).{8,}$/; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*import-in-the-middle* 2 | public-hoist-pattern[]=*require-in-the-middle* -------------------------------------------------------------------------------- /public/images/trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/trophy.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/shared/lib/version.ts: -------------------------------------------------------------------------------- 1 | import pkg from "../../../package.json"; 2 | 3 | export const appVersion = pkg.version; -------------------------------------------------------------------------------- /public/images/calculator-og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/calculator-og.jpg -------------------------------------------------------------------------------- /public/images/edwardsoyfit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/edwardsoyfit.png -------------------------------------------------------------------------------- /public/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/favicon-32x32.png -------------------------------------------------------------------------------- /src/shared/constants/success.ts: -------------------------------------------------------------------------------- 1 | export const SUCCESS_MESSAGES = { 2 | DELETE_SUCCESS: "DELETE_SUCCESS", 3 | }; 4 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/images/coachs/mathias.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/coachs/mathias.png -------------------------------------------------------------------------------- /public/images/coachs/mathias2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/coachs/mathias2.jpg -------------------------------------------------------------------------------- /public/images/default-workout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/default-workout.jpg -------------------------------------------------------------------------------- /public/images/equipment/band.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/equipment/band.png -------------------------------------------------------------------------------- /public/images/equipment/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/equipment/bench.png -------------------------------------------------------------------------------- /public/images/equipment/plate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/equipment/plate.png -------------------------------------------------------------------------------- /public/images/nutripure-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/nutripure-logo.webp -------------------------------------------------------------------------------- /src/shared/constants/cookies.ts: -------------------------------------------------------------------------------- 1 | export const Cookies = { 2 | TrackingConsent: "tracking-consent", 3 | } as const; 4 | -------------------------------------------------------------------------------- /public/images/equipment/barbell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/equipment/barbell.png -------------------------------------------------------------------------------- /public/images/equipment/dumbbell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/equipment/dumbbell.png -------------------------------------------------------------------------------- /public/images/nutripure-gellules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/nutripure-gellules.png -------------------------------------------------------------------------------- /src/features/auth/verify-email/constants.ts: -------------------------------------------------------------------------------- 1 | export const COUNTDOWN_TIME = process.env.NODE_ENV === "development" ? 3 : 60; 2 | -------------------------------------------------------------------------------- /public/images/default-og-image_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/default-og-image_en.jpg -------------------------------------------------------------------------------- /public/images/default-og-image_es.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/default-og-image_es.jpg -------------------------------------------------------------------------------- /public/images/default-og-image_fr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/default-og-image_fr.jpg -------------------------------------------------------------------------------- /public/images/default-og-image_pt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/default-og-image_pt.jpg -------------------------------------------------------------------------------- /public/images/default-og-image_ru.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/default-og-image_ru.jpg -------------------------------------------------------------------------------- /public/images/default-og-image_zh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/default-og-image_zh.jpg -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolCry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolCry.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolLoL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolLoL.png -------------------------------------------------------------------------------- /public/images/equipment/bodyweight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/equipment/bodyweight.png -------------------------------------------------------------------------------- /public/images/equipment/kettlebell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/equipment/kettlebell.png -------------------------------------------------------------------------------- /public/images/equipment/pull-up-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/equipment/pull-up-bar.png -------------------------------------------------------------------------------- /public/images/nutripure-gellules-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/nutripure-gellules-2.png -------------------------------------------------------------------------------- /public/images/placeholders/no-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/placeholders/no-image.jpg -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // add opera to the user agent 2 | declare global { 3 | interface Navigator { 4 | opera: any; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/signout/page.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthSignOutPage() { 2 | return
AuthSignOutPage
; 3 | } 4 | -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolChief.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolChief.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolHappy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolHappy.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolHuuuu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolHuuuu.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolLove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolLove.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolMeeeh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolMeeeh.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolRich.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolRich.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolSwag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolSwag.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolTong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolTong.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolWooow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolWooow.png -------------------------------------------------------------------------------- /public/images/old-nutripure-gellules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/old-nutripure-gellules.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolBiceps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolBiceps.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolMedical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolMedical.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolPolice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolPolice.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolShoked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolShoked.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolShoked2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolShoked2.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolTeeths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolTeeths.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolYeahOk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolYeahOk.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolhaphap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolhaphap.png -------------------------------------------------------------------------------- /public/images/placeholders/coach-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/placeholders/coach-avatar.png -------------------------------------------------------------------------------- /locales/types.ts: -------------------------------------------------------------------------------- 1 | export const locales = ["en", "fr", "es", "zh-CN", "ru", "pt"] as const; 2 | export type Locale = (typeof locales)[number]; 3 | -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolEmbarassed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolEmbarassed.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolExhausted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolExhausted.png -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolKittyGirl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolKittyGirl.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-sort-json"], 3 | "printWidth": 140, 4 | "proseWrap": "always", 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /app/[locale]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Page404 } from "@/widgets/404"; 2 | 3 | export default function NotFoundPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /public/images/emojis/WorkoutCoolDisapointed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/emojis/WorkoutCoolDisapointed.png -------------------------------------------------------------------------------- /public/images/screenshots/heart-rate-zones/en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/screenshots/heart-rate-zones/en.jpg -------------------------------------------------------------------------------- /public/images/screenshots/heart-rate-zones/es.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/screenshots/heart-rate-zones/es.jpg -------------------------------------------------------------------------------- /public/images/screenshots/heart-rate-zones/fr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/screenshots/heart-rate-zones/fr.jpg -------------------------------------------------------------------------------- /public/images/screenshots/heart-rate-zones/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/screenshots/heart-rate-zones/og.jpg -------------------------------------------------------------------------------- /public/images/screenshots/heart-rate-zones/pt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/screenshots/heart-rate-zones/pt.jpg -------------------------------------------------------------------------------- /public/images/screenshots/heart-rate-zones/ru.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/screenshots/heart-rate-zones/ru.jpg -------------------------------------------------------------------------------- /src/features/consent-banner/schema/tracking-consent.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const trackingConsentSchema = z.boolean(); 4 | -------------------------------------------------------------------------------- /src/shared/lib/format.ts: -------------------------------------------------------------------------------- 1 | export function nullToUndefined(value: T | null): T | undefined { 2 | return value === null ? undefined : value; 3 | } 4 | -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthLayout({ children }: { children: React.ReactNode }) { 2 | return <>{children}; 3 | } 4 | -------------------------------------------------------------------------------- /public/images/screenshots/heart-rate-zones/zh-CN.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snouzy/workout-cool/HEAD/public/images/screenshots/heart-rate-zones/zh-CN.jpg -------------------------------------------------------------------------------- /app/[locale]/(admin)/admin/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Page404 } from "@/widgets/404"; 2 | 3 | export default function NotFoundPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/schemas/url.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const urlSchema = z.string().url("form_invalid_url"); 4 | export type Url = z.infer; 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "typescript.tsdk": "node_modules/typescript/lib" 6 | } 7 | -------------------------------------------------------------------------------- /app/[locale]/(admin)/admin/[...catchAll]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Page404 } from "@/widgets/404"; 2 | 3 | export default function AdminCatchAll() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/constants/screen.ts: -------------------------------------------------------------------------------- 1 | export const Screens = { 2 | linkInBio: "link-in-bio", 3 | dashboard: "dashboard", 4 | website: "website", 5 | editor: "editor", 6 | }; 7 | -------------------------------------------------------------------------------- /app/[locale]/(admin)/admin/[...catchAll]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Page404 } from "@/widgets/404"; 2 | 3 | export default function NotFoundPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250626205904_remove_payment_table_keep_ispremium/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "user" ADD COLUMN "isPremium" BOOLEAN DEFAULT false; 3 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/migrations_backup/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /src/shared/lib/guards.ts: -------------------------------------------------------------------------------- 1 | export const isObject = (value: unknown): value is Record => { 2 | return typeof value === "object" && value !== null && !Array.isArray(value); 3 | }; 4 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250708214116_add_rating_to_wkt_sessions/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "workout_sessions" ADD COLUMN "rating" INTEGER, 3 | ADD COLUMN "ratingComment" TEXT; 4 | -------------------------------------------------------------------------------- /src/features/release-notes/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { ChangelogNotificationBadge } from "./changelog-notification-badge"; 2 | export type { ChangelogNotificationBadgeProps } from "./changelog-notification-badge"; -------------------------------------------------------------------------------- /app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { toNextJsHandler } from "better-auth/next-js"; 2 | 3 | import { auth } from "@/features/auth/lib/better-auth"; 4 | 5 | export const { POST, GET } = toNextJsHandler(auth); 6 | -------------------------------------------------------------------------------- /src/shared/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { twMerge } from "tailwind-merge"; 2 | import { clsx, type ClassValue } from "clsx"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root; 6 | 7 | export { AspectRatio }; 8 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250414232816_add_first_name_and_last_name/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "user" ADD COLUMN "firstName" TEXT NOT NULL DEFAULT '', 3 | ADD COLUMN "lastName" TEXT NOT NULL DEFAULT ''; 4 | -------------------------------------------------------------------------------- /src/features/release-notes/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useChangelogNotification } from "./use-changelog-notification"; 2 | export type { UseChangelogNotificationReturn, ChangelogNotificationConfig } from "./use-changelog-notification"; -------------------------------------------------------------------------------- /src/features/update-password/lib/validate-password.ts: -------------------------------------------------------------------------------- 1 | import { PASSWORD_REGEX } from "@/shared/constants/regexs"; 2 | 3 | export const validatePassword = (password: string) => { 4 | return PASSWORD_REGEX.test(password); 5 | }; 6 | -------------------------------------------------------------------------------- /src/features/email/email.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const EmailActionSchema = z.object({ 4 | email: z.string().email(), 5 | }); 6 | 7 | export type EmailActionSchemaType = z.infer; 8 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250615160343_add_muscle_to_a_workout_session/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "workout_sessions" ADD COLUMN "muscles" "ExerciseAttributeValueEnum"[] DEFAULT ARRAY[]::"ExerciseAttributeValueEnum"[]; 3 | -------------------------------------------------------------------------------- /src/entities/user/schemas/get-user.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const getUsersSchema = z.object({ 4 | page: z.number().default(1), 5 | limit: z.number().default(10), 6 | search: z.string().optional(), 7 | }); 8 | -------------------------------------------------------------------------------- /src/features/workout-session/model/use-workout-session.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useWorkoutSessionStore } from "./workout-session.store"; 4 | 5 | export function useWorkoutSession() { 6 | return useWorkoutSessionStore(); 7 | } 8 | -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/(auth-layout)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { CredentialsLoginForm } from "@/features/auth/signin/ui/CredentialsLoginForm"; 2 | 3 | export default async function AuthSignInPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /workout-cool.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "../workout-cool-mobile" 8 | } 9 | ], 10 | "settings": { 11 | "typescript.tsdk": "node_modules/typescript/lib" 12 | } 13 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Workout Cool Discord 4 | url: https://discord.gg/NtrsUBuHUB 5 | about: Please use our Discord server for all questions, discussions, and support. -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/(auth-layout)/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResetPasswordForm } from "@/features/auth/reset-password/ui/reset-password-form"; 2 | 3 | export default function ResetPasswordPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/verify-email/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { VerifyEmailPage } from "@/features/auth/verify-email/ui/verify-email-page"; 4 | 5 | export default function VerifyEmailRootPage() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /app/[locale]/(app)/onboarding/layout.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutParams } from "@/shared/types/next"; 2 | 3 | export default async function OnboardingLayout(props: LayoutParams<{}>) { 4 | // TODO: add onboarding logic 5 | 6 | return props.children; 7 | } 8 | -------------------------------------------------------------------------------- /app/[locale]/(app)/onboarding/page.tsx: -------------------------------------------------------------------------------- 1 | export default async function OnboardingPage() { 2 | return ( 3 |
4 |
Onboarding
5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/(auth-layout)/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordForm } from "@/features/auth/forgot-password/ui/forgot-password-form"; 2 | 3 | export default async function ForgotPasswordPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/features/auth/signin/schema/signin.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const loginSchema = z.object({ 4 | email: z.string().email(), 5 | password: z.string().min(6), 6 | }); 7 | 8 | export type LoginSchema = z.infer; 9 | -------------------------------------------------------------------------------- /src/features/update-password/model/update-password.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const UpdatePasswordSchema = z.object({ 4 | currentPassword: z.string().min(8), 5 | newPassword: z.string().min(8), 6 | confirmPassword: z.string().min(8), 7 | }); 8 | -------------------------------------------------------------------------------- /src/shared/lib/logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "tslog"; 2 | 3 | export const logger = new Logger({ 4 | name: "AppLogger", 5 | // Don't use `env` here, because we can use the logger in the browser 6 | minLevel: process.env.NODE_ENV === "production" ? 3 : 0, 7 | }); 8 | -------------------------------------------------------------------------------- /src/features/auth/forgot-password/forgot-password.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const forgotPasswordSchema = z.object({ 4 | email: z.string().email("Adresse e-mail invalide"), 5 | }); 6 | 7 | export type ForgotPasswordSchema = z.infer; 8 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250625195907_add_program_visibility/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "ProgramVisibility" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "programs" ADD COLUMN "visibility" "ProgramVisibility" NOT NULL DEFAULT 'DRAFT'; 6 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250626134345_remove_emoji_on_program/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `emoji` on the `programs` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "programs" DROP COLUMN "emoji"; 9 | -------------------------------------------------------------------------------- /src/features/leaderboard/models/types.ts: -------------------------------------------------------------------------------- 1 | export interface TopWorkoutUser { 2 | userId: string; 3 | userName: string; 4 | userImage: string | null; 5 | totalWorkouts: number; 6 | lastWorkoutAt: Date | null; 7 | averageWorkoutsPerWeek: number; 8 | memberSince: Date; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/version.tsx: -------------------------------------------------------------------------------- 1 | import { appVersion } from "@/shared/lib/version"; 2 | 3 | export const Version = () => ( 4 |
5 | 6 | v{appVersion} 7 | 8 |
9 | 10 | ); -------------------------------------------------------------------------------- /src/features/contact/support/contact-support.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const ContactSupportSchema = z.object({ 4 | email: z.string(), 5 | subject: z.string(), 6 | message: z.string(), 7 | }); 8 | 9 | export type ContactSupportSchemaType = z.infer; 10 | -------------------------------------------------------------------------------- /src/shared/types/storage.ts: -------------------------------------------------------------------------------- 1 | export interface ImageUploader { 2 | (params: { fileBuffer: Buffer; fileName: string; mimeType: string; bucket: string }): Promise<{ fileName: string; url: string }>; 3 | } 4 | 5 | export interface ImageDeleter { 6 | (params: { fileName: string; bucket: string }): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/entities/user/model/useCurrentUser.ts: -------------------------------------------------------------------------------- 1 | import { useSession } from "@/features/auth/lib/auth-client"; 2 | 3 | export const useCurrentUser = () => { 4 | const session = useSession(); 5 | const user = session.data?.user; 6 | 7 | if (!user) { 8 | return null; 9 | } 10 | 11 | return user; 12 | }; 13 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20240726_simplify_subscription_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- Simplify Subscription model by removing unnecessary RevenueCat fields 2 | ALTER TABLE "subscriptions" 3 | DROP COLUMN IF EXISTS "revenueCatTransactionId", 4 | DROP COLUMN IF EXISTS "revenueCatProductId", 5 | DROP COLUMN IF EXISTS "revenueCatEntitlement"; -------------------------------------------------------------------------------- /src/shared/constants/placeholders.ts: -------------------------------------------------------------------------------- 1 | export const PLACEHOLDERS = { 2 | PROFILE_IMAGE: "/images/placeholders/coach-avatar.png", 3 | HERO_IMAGE: "/images/placeholders/hero-image.png", 4 | DESIGN_BACKGROUND_IMAGE: "/images/placeholders/design-background-image.png", 5 | DEFAULT_HERO_IMAGE: "/images/patterns/7.png", 6 | }; 7 | -------------------------------------------------------------------------------- /src/entities/user/model/useCurrentSession.ts: -------------------------------------------------------------------------------- 1 | import { useSession } from "@/features/auth/lib/auth-client"; 2 | 3 | export const useCurrentSession = () => { 4 | const session = useSession(); 5 | const sessionData = session.data; 6 | 7 | if (!sessionData) { 8 | return null; 9 | } 10 | 11 | return sessionData; 12 | }; 13 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250623143952_remove_webhook_events/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `webhook_events` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "webhook_events"; 9 | 10 | -- DropEnum 11 | DROP TYPE "WebhookSource"; 12 | -------------------------------------------------------------------------------- /src/components/ui/loader.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | 3 | import { cn } from "@/shared/lib/utils"; 4 | 5 | import type { LucideProps } from "lucide-react"; 6 | 7 | export const Loader = ({ className, ...props }: LucideProps) => { 8 | return ; 9 | }; 10 | -------------------------------------------------------------------------------- /src/shared/constants/workout-set-types.ts: -------------------------------------------------------------------------------- 1 | export const AVAILABLE_WORKOUT_SET_TYPES = ["TIME", "WEIGHT", "REPS", "BODYWEIGHT"] as const; 2 | export const ALL_WORKOUT_SET_TYPES = ["TIME", "WEIGHT", "REPS", "BODYWEIGHT", "NA"] as const; 3 | export const MAX_WORKOUT_SET_COLUMNS = 4; 4 | export const WORKOUT_SET_UNITS_TUPLE = ["kg", "lbs"] as const; 5 | -------------------------------------------------------------------------------- /src/features/ads/hooks/useUserSubscription.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSession } from "@/features/auth/lib/auth-client"; 4 | 5 | export function useUserSubscription() { 6 | const { data: session, ...rest } = useSession(); 7 | const isPremium = session?.user?.isPremium || false; 8 | 9 | return { isPremium, ...rest }; 10 | } 11 | -------------------------------------------------------------------------------- /src/features/contact-feedback/model/contact-feedback.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const ContactFeedbackSchema = z.object({ 4 | email: z.string().optional(), 5 | review: z.string().optional(), 6 | message: z.string(), 7 | }); 8 | 9 | export type ContactFeedbackSchemaType = z.infer; 10 | -------------------------------------------------------------------------------- /src/features/theme/ThemeProviders.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider, ThemeProviderProps } from "next-themes"; 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ui/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | 6 | export function ThemeProvider({ children, ...props }: React.ComponentProps) { 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /src/entities/user/types/session-user.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | 3 | import { authClient } from "@/features/auth/lib/auth-client"; 4 | 5 | export interface SessionUser extends Omit { 6 | image?: string | null; 7 | } 8 | 9 | export type Session = typeof authClient.$Infer.Session; 10 | -------------------------------------------------------------------------------- /src/features/auth/signup/schema/signup.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const signUpSchema = z.object({ 4 | firstName: z.string(), 5 | lastName: z.string(), 6 | email: z.string().email(), 7 | password: z.string().min(8), 8 | verifyPassword: z.string().min(8), 9 | }); 10 | 11 | export type SignUpSchema = z.infer; 12 | -------------------------------------------------------------------------------- /locales/server.ts: -------------------------------------------------------------------------------- 1 | import { createI18nServer } from "next-international/server"; 2 | 3 | export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({ 4 | en: () => import("./en"), 5 | fr: () => import("./fr"), 6 | es: () => import("./es"), 7 | "zh-CN": () => import("./zh-CN"), 8 | ru: () => import("./ru"), 9 | pt: () => import("./pt") 10 | }); -------------------------------------------------------------------------------- /nextauth.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-definitions */ 2 | import type { DefaultSession } from "next-auth"; 3 | 4 | declare module "next-auth" { 5 | interface Session { 6 | user: DefaultSession["user"] & { 7 | id: string; 8 | email: string; 9 | name?: string; 10 | image?: string; 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/features/update-password/lib/hash.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | export const hashStringWithSalt = (string: string, salt: string) => { 3 | const hash = crypto.createHash("sha256"); 4 | 5 | const saltedString = salt + string; 6 | 7 | hash.update(saltedString); 8 | 9 | const hashedString = hash.digest("hex"); 10 | 11 | return hashedString; 12 | }; 13 | -------------------------------------------------------------------------------- /app/[locale]/(admin)/admin/settings/page.tsx: -------------------------------------------------------------------------------- 1 | export default function AdminSettings() { 2 | return ( 3 |
4 |
5 |

Settings

6 |

Configuration and administration of the system.

7 |
8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/[locale]/(app)/[slug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | 3 | interface RootLayoutProps { 4 | params: Promise<{ locale: string }>; 5 | children: ReactElement; 6 | } 7 | 8 | export default async function RootLayout({ children }: RootLayoutProps) { 9 | 10 | return ( 11 |
12 | {children} 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/features/statistics/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ExerciseCharts } from "./ExerciseStatisticsTab"; 2 | export { WeightProgressionChart } from "./WeightProgressionChart"; 3 | export { OneRepMaxChart } from "./OneRepMaxChart"; 4 | export { VolumeChart } from "./VolumeChart"; 5 | export { TimeframeSelector } from "./TimeframeSelector"; 6 | export { ExerciseSelection } from "./ExerciseSelection"; 7 | -------------------------------------------------------------------------------- /src/entities/user/schemas/update-user.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const updateUserSchema = z.object({ 4 | locale: z.string().optional(), 5 | firstName: z.string().optional(), 6 | lastName: z.string().optional(), 7 | image: z.string().optional(), 8 | revalidatePath: z.string().optional(), 9 | }); 10 | 11 | export type UpdateUserInput = z.infer; 12 | -------------------------------------------------------------------------------- /src/components/ads/HorizontalTopBanner.tsx: -------------------------------------------------------------------------------- 1 | import { HorizontalAdBanner } from "./HorizontalAdBanner"; 2 | 3 | interface HorizontalTopBannerProps { 4 | adSlot?: string; 5 | ezoicPlacementId?: string; 6 | } 7 | 8 | export function HorizontalTopBanner({ adSlot, ezoicPlacementId }: HorizontalTopBannerProps) { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /src/features/layout/model/use-sidebar.store.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface SidebarState { 4 | currentPageId: string | null; 5 | setCurrentPageId: (currentPageId: string | null) => void; 6 | } 7 | 8 | export const useSidebarStore = create()((set) => ({ 9 | currentPageId: null, 10 | setCurrentPageId: (currentPageId) => set(() => ({ currentPageId })), 11 | })); 12 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250613095031_add_multi_column_support/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "WorkoutSet" ADD COLUMN "types" "WorkoutSetType"[] DEFAULT ARRAY[]::"WorkoutSetType"[], 3 | ADD COLUMN "units" "WorkoutSetUnit"[] DEFAULT ARRAY[]::"WorkoutSetUnit"[], 4 | ADD COLUMN "valuesInt" INTEGER[] DEFAULT ARRAY[]::INTEGER[], 5 | ADD COLUMN "valuesSec" INTEGER[] DEFAULT ARRAY[]::INTEGER[]; 6 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 12 | -------------------------------------------------------------------------------- /src/shared/constants/paths.ts: -------------------------------------------------------------------------------- 1 | export const paths = { 2 | root: "/", 3 | signUp: "auth/signup", 4 | signIn: "auth/signin", 5 | forgotPassword: "auth/forgot-password", 6 | resetPassword: "auth/reset-password", 7 | verifyEmail: "auth/verify-email", 8 | profile: "profile", 9 | privacy: "/legal/privacy", 10 | terms: "/legal/terms", 11 | programs: "/programs", 12 | leaderboard: "/leaderboard", 13 | } as const; 14 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250626204136_remove_payment_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `payments` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "payments" DROP CONSTRAINT "payments_subscriptionId_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "payments"; 12 | 13 | -- DropEnum 14 | DROP TYPE "PaymentStatus"; 15 | -------------------------------------------------------------------------------- /src/features/auth/ui/SignUpButton.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { getI18n } from "locales/server"; 4 | import { Button } from "@/components/ui/button"; 5 | 6 | export const SignUpButton = async () => { 7 | const t = await getI18n(); 8 | 9 | return ( 10 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/features/workout-builder/index.ts: -------------------------------------------------------------------------------- 1 | export { WorkoutStepper } from "./ui/workout-stepper"; 2 | export { useWorkoutStepper } from "./hooks/use-workout-stepper"; 3 | export { useWorkoutSession } from "../workout-session/model/use-workout-session"; 4 | export { EQUIPMENT_CONFIG, getEquipmentByValue, getEquipmentLabel } from "./model/equipment-config"; 5 | export type { WorkoutBuilderState, WorkoutBuilderStep, EquipmentItem } from "./types"; 6 | -------------------------------------------------------------------------------- /src/shared/lib/revenuecat/index.ts: -------------------------------------------------------------------------------- 1 | // RevenueCat integration exports 2 | export { RevenueCatConfig } from "./revenuecat.config"; 3 | export { RevenueCatMapping } from "./revenuecat.mapping"; 4 | export { RevenueCatApiClient, revenueCatApi } from "./revenuecat.api"; 5 | 6 | // Test that our files compile correctly 7 | export type { } from "./revenuecat.config"; 8 | export type { RevenueCatSubscriber, RevenueCatApiError } from "./revenuecat.api"; -------------------------------------------------------------------------------- /app/[locale]/@modal/(.)auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { CredentialsLoginForm } from "@/features/auth/signin/ui/CredentialsLoginForm"; 2 | import { Dialog, DialogContent } from "@/components/ui/dialog"; 3 | 4 | export default function LoginModal() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/[locale]/(app)/tools/calorie-calculator/shared/types/index.ts: -------------------------------------------------------------------------------- 1 | // Calorie calculator types for better type safety 2 | export type CalculatorFormula = "mifflin" | "harris" | "katch" | "cunningham" | "oxford"; 3 | 4 | export interface CalculatorConfig { 5 | formula: CalculatorFormula; 6 | requiresBodyFat: boolean; 7 | name: string; 8 | description: string; 9 | buttonGradient: { 10 | from: string; 11 | to: string; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250505114841_add_user_role/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "session" ADD COLUMN "impersonatedBy" TEXT; 6 | 7 | -- AlterTable 8 | ALTER TABLE "user" ADD COLUMN "banExpires" TIMESTAMP(3), 9 | ADD COLUMN "banReason" TEXT, 10 | ADD COLUMN "banned" BOOLEAN DEFAULT false, 11 | ADD COLUMN "role" "UserRole" DEFAULT 'USER'; 12 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250626205121_remove_legacy_premium_fields/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `isPremium` on the `user` table. All the data in the column will be lost. 5 | - You are about to drop the column `premiumUntil` on the `user` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "user" DROP COLUMN "isPremium", 10 | DROP COLUMN "premiumUntil"; 11 | -------------------------------------------------------------------------------- /src/features/auth/ui/SignInButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | 4 | import { useI18n } from "locales/client"; 5 | import { Button } from "@/components/ui/button"; 6 | 7 | export const SignInButton = () => { 8 | const t = useI18n(); 9 | 10 | return ( 11 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/svg/Youtube.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import * as React from "react"; 3 | 4 | export const YoutubeIcon: React.FC> = (props) => ( 5 | Youtube 13 | ); 14 | -------------------------------------------------------------------------------- /src/shared/constants/errors.ts: -------------------------------------------------------------------------------- 1 | export const ERROR_MESSAGES = { 2 | USER_NOT_FOUND: "USER_NOT_FOUND", 3 | PAGE_NOT_FOUND: "PAGE_NOT_FOUND", 4 | UNAUTHORIZED: "UNAUTHORIZED", 5 | USERNAME_ALREADY_TAKEN: "USERNAME_ALREADY_TAKEN", 6 | INVALID_CURRENT_PASSWORD: "INVALID_CURRENT_PASSWORD", 7 | INVALID_NEW_PASSWORD: "INVALID_NEW_PASSWORD", 8 | PASSWORDS_DO_NOT_MATCH: "PASSWORDS_DO_NOT_MATCH", 9 | EMAIL_ALREADY_USED: "EMAIL_ALREADY_USED", 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/ads/HorizontalBottomBanner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { HorizontalAdBanner } from "@/components/ads/HorizontalAdBanner"; 4 | 5 | interface HorizontalBottomBannerProps { 6 | adSlot?: string; 7 | ezoicPlacementId?: string; 8 | } 9 | 10 | export function HorizontalBottomBanner({ adSlot, ezoicPlacementId }: HorizontalBottomBannerProps) { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /src/features/workout-session/hooks/use-donation-modal.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | export function useDonationModal() { 6 | const [showModal, setShowModal] = useState(false); 7 | 8 | const openModal = () => { 9 | setShowModal(true); 10 | }; 11 | 12 | const closeModal = () => { 13 | setShowModal(false); 14 | }; 15 | 16 | return { 17 | showModal, 18 | openModal, 19 | closeModal, 20 | }; 21 | } -------------------------------------------------------------------------------- /src/features/workout-builder/hooks/use-workout-session.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useWorkoutSessionStore } from "@/features/workout-session/model/workout-session.store"; 4 | 5 | export function useWorkoutSession() { 6 | // Le paramètre sessionId n'est plus utilisé ici, la logique de persistance reste dans workoutSessionLocal 7 | // (si besoin, on peut l'utiliser pour charger une session spécifique dans le store) 8 | return useWorkoutSessionStore(); 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/styles/additional-styles/utility-patterns.css: -------------------------------------------------------------------------------- 1 | /* Typography */ 2 | * { 3 | } 4 | .h1 { 5 | @apply text-5xl font-extrabold; 6 | } 7 | 8 | .h2 { 9 | @apply text-4xl font-extrabold; 10 | } 11 | 12 | .h3 { 13 | @apply text-3xl font-extrabold; 14 | } 15 | 16 | .h4 { 17 | @apply text-2xl font-extrabold; 18 | } 19 | 20 | @media (min-width: 768px) { 21 | .h1 { 22 | @apply text-6xl; 23 | } 24 | 25 | .h2 { 26 | @apply text-5xl; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/svg/Calendly.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import * as React from "react"; 3 | 4 | // On utilise l'image SVG du dossier public 5 | export const CalendlyIcon: React.FC> = (props) => ( 6 | Calendly 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/ui/title-with-dot.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/shared/lib/utils"; 2 | 3 | export function TitleWithDot({ title, className }: { title: string; className?: string }) { 4 | return ( 5 |
6 | 7 |

{title}

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/api/programs/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { getPublicPrograms } from "@/features/programs/actions/get-public-programs.action"; 4 | 5 | export async function GET() { 6 | try { 7 | const programs = await getPublicPrograms(); 8 | return NextResponse.json(programs); 9 | } catch (error) { 10 | console.error("Error fetching programs:", error); 11 | return NextResponse.json({ error: "Failed to fetch programs" }, { status: 500 }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/svg/IconCheckboxCheck.tsx: -------------------------------------------------------------------------------- 1 | const IconCheckboxCheck = ({ className }: any) => { 2 | return ( 3 | 4 | 11 | 12 | ); 13 | }; 14 | export default IconCheckboxCheck; 15 | -------------------------------------------------------------------------------- /src/features/workout-session/lib/workout-set-labels.ts: -------------------------------------------------------------------------------- 1 | import { TFunction } from "locales/client"; 2 | 3 | import { WorkoutSetType } from "../types/workout-set"; 4 | 5 | export function getWorkoutSetTypeLabels(t: TFunction): Record { 6 | return { 7 | TIME: t("workout_builder.session.time"), 8 | WEIGHT: t("workout_builder.session.weight"), 9 | REPS: t("workout_builder.session.reps"), 10 | BODYWEIGHT: t("workout_builder.session.bodyweight"), 11 | NA: "N/A", 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient(); 5 | }; 6 | 7 | type PrismaClientSingleton = ReturnType; 8 | 9 | const globalForPrisma = globalThis as unknown as { 10 | prisma: PrismaClientSingleton | undefined; 11 | }; 12 | 13 | export const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); 14 | 15 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 16 | -------------------------------------------------------------------------------- /src/components/ads/index.ts: -------------------------------------------------------------------------------- 1 | export { GoogleAdSense } from "./GoogleAdSense"; 2 | export { AdWrapper } from "./AdWrapper"; 3 | export { VerticalAdBanner } from "./VerticalAdBanner"; 4 | export { VerticalLeftBanner } from "./VerticalLeftBanner"; 5 | export { VerticalRightBanner } from "./VerticalRightBanner"; 6 | export { HorizontalTopBanner } from "./HorizontalTopBanner"; 7 | export { HorizontalBottomBanner } from "./HorizontalBottomBanner"; 8 | export { AdBlockerForPremium } from "./AdBlockerForPremium"; 9 | export { InArticle } from "./InArticle"; 10 | -------------------------------------------------------------------------------- /src/components/svg/UnderlineSvg.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from "react"; 2 | 3 | export const UnderlineSvg = (props: ComponentPropsWithoutRef<"svg">) => { 4 | return ( 5 | 6 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/features/release-notes/index.ts: -------------------------------------------------------------------------------- 1 | export { ReleaseNotesDialog } from "./ui/release-notes-dialog"; 2 | export { ChangelogNotificationBadge } from "./ui/changelog-notification-badge"; 3 | export { useChangelogNotification } from "./hooks/use-changelog-notification"; 4 | export type { ReleaseNotesDialogProps } from "./ui/release-notes-dialog"; 5 | export type { ChangelogNotificationBadgeProps } from "./ui/changelog-notification-badge"; 6 | export type { UseChangelogNotificationReturn, ChangelogNotificationConfig } from "./hooks/use-changelog-notification"; 7 | -------------------------------------------------------------------------------- /src/shared/lib/location/location.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | 3 | import { EU_COUNTRY_CODES } from "@/shared/lib/location/eu-countries"; 4 | 5 | export async function getCountryCode() { 6 | const headersList = await headers(); 7 | 8 | return headersList.get("x-vercel-ip-country") || "SE"; 9 | } 10 | 11 | export async function isEU() { 12 | const countryCode = await getCountryCode(); 13 | 14 | if (countryCode && EU_COUNTRY_CODES.includes(countryCode)) { 15 | return true; 16 | } 17 | 18 | return false; 19 | } 20 | -------------------------------------------------------------------------------- /src/features/workout-builder/schema/get-exercises.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { ExerciseAttributeValueEnum } from "@prisma/client"; 3 | 4 | export const getExercisesSchema = z.object({ 5 | equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)).min(1, "Au moins un équipement est requis"), 6 | muscles: z.array(z.nativeEnum(ExerciseAttributeValueEnum)).min(1, "Au moins un muscle est requis"), 7 | limit: z.number().int().min(1).max(10).default(3), 8 | }); 9 | 10 | export type GetExercisesInput = z.infer; 11 | -------------------------------------------------------------------------------- /src/features/layout/page-heading.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/shared/lib/utils"; 2 | import { Card, CardContent } from "@/components/ui/card"; 3 | 4 | type PageHeadingProps = { 5 | heading: string; 6 | className?: string; 7 | }; 8 | 9 | const PageHeading = ({ heading, className }: PageHeadingProps) => { 10 | return ( 11 | 12 | {heading} 13 | 14 | ); 15 | }; 16 | 17 | export default PageHeading; 18 | -------------------------------------------------------------------------------- /src/features/premium/ui/index.ts: -------------------------------------------------------------------------------- 1 | // Premium UI Components 2 | export { PremiumUpgradeCard } from "./premium-upgrade-card"; 3 | export { ConversionFlowNotification } from "./conversion-flow-notification"; 4 | 5 | // New Pricing Page Components 6 | export { PricingHeroSection } from "./pricing-hero-section"; 7 | export { FeatureComparisonTable } from "./feature-comparison-table"; 8 | export { PricingFAQ } from "./pricing-faq"; 9 | export { PricingTestimonials } from "./pricing-testimonials"; 10 | 11 | // Note: Add other premium components exports here as needed 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "aliases": { 4 | "components": "@/components", 5 | "utils": "@/shared/lib/utils", 6 | "ui": "@/components/ui", 7 | "lib": "@/shared/lib", 8 | "hooks": "@/hooks" 9 | }, 10 | "iconLibrary": "lucide", 11 | "rsc": true, 12 | "style": "new-york", 13 | "tailwind": { 14 | "config": "tailwind.config.ts", 15 | "css": "app/css/globals.css", 16 | "baseColor": "neutral", 17 | "cssVariables": true, 18 | "prefix": "" 19 | }, 20 | "tsx": true 21 | } 22 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | reactStrictMode: true, 5 | images: { 6 | unoptimized: true, 7 | domains: ["lh3.googleusercontent.com", "192.168.1.12", "localhost", "www.facebook.com", "api.dicebear.com"], 8 | remotePatterns: [ 9 | { 10 | protocol: "https", 11 | hostname: "**.vercel.app", 12 | }, 13 | { 14 | protocol: "https", 15 | hostname: "api.dicebear.com", 16 | }, 17 | ], 18 | }, 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250414170807_add_feedbacks/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Feedback" ( 3 | "id" TEXT NOT NULL, 4 | "review" INTEGER NOT NULL, 5 | "message" TEXT NOT NULL, 6 | "email" TEXT, 7 | "userId" TEXT, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" TIMESTAMP(3) NOT NULL, 10 | 11 | CONSTRAINT "Feedback_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "Feedback" ADD CONSTRAINT "Feedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📝 Description 2 | 3 | 4 | 5 | ## 📋 Checklist 6 | 7 | - [ ] My code follows the project conventions 8 | - [ ] This PR includes breaking changes 9 | - [ ] I have updated documentation if necessary 10 | 11 | ## 🗃️ Prisma Migrations (if applicable) 12 | 13 | - [ ] I have created a migration 14 | - [ ] I have tested the migration locally 15 | 16 | ## 📸 Screenshots (if applicable) 17 | 18 | 19 | 20 | ## 🔗 Related Issues 21 | 22 | 23 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250614153656_remove_value_int_value_sec_unit_from_workoutset/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `unit` on the `workout_sets` table. All the data in the column will be lost. 5 | - You are about to drop the column `valueInt` on the `workout_sets` table. All the data in the column will be lost. 6 | - You are about to drop the column `valueSec` on the `workout_sets` table. All the data in the column will be lost. 7 | 8 | */ 9 | -- AlterTable 10 | ALTER TABLE "workout_sets" DROP COLUMN "unit", 11 | DROP COLUMN "valueInt", 12 | DROP COLUMN "valueSec"; 13 | -------------------------------------------------------------------------------- /src/features/workout-builder/model/favorite-exercises-synchronizer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | import { useSession } from "@/features/auth/lib/auth-client"; 6 | 7 | import { useSyncFavoriteExercises } from "../hooks/use-sync-favorite-exercises"; 8 | 9 | export function FavoriteExercisesSynchronizer() { 10 | const { syncFavoriteExercises } = useSyncFavoriteExercises(); 11 | const { data: session } = useSession(); 12 | 13 | useEffect(() => { 14 | if (session?.user) { 15 | syncFavoriteExercises(); 16 | } 17 | }, [session?.user]); 18 | 19 | return null; 20 | } 21 | -------------------------------------------------------------------------------- /app/[locale]/(app)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import { getLocalizedMdx } from "@/shared/lib/mdx/load-mdx"; 2 | 3 | type PageProps = { 4 | params: Promise<{ locale: string }>; 5 | }; 6 | 7 | export default async function AboutPage({ params }: PageProps) { 8 | const { locale } = await params; 9 | const content = await getLocalizedMdx("about", locale); 10 | 11 | return ( 12 |
13 |
14 |
{content}
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ads/AdPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | interface AdPlaceholderProps { 2 | width: string; 3 | height: string; 4 | type?: string; 5 | } 6 | 7 | export function AdPlaceholder({ width, height, type = "Ad" }: AdPlaceholderProps) { 8 | return ( 9 |
13 | 14 | {type} {width} × {height} 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/features/auth/model/useLogout.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 5 | 6 | import { authClient } from "@/features/auth/lib/auth-client"; 7 | 8 | export const useLogout = (redirectUrl: string = "/") => { 9 | const router = useRouter(); 10 | const queryClient = useQueryClient(); 11 | 12 | return useMutation({ 13 | mutationFn: async () => { 14 | await authClient.signOut(); 15 | router.push(redirectUrl); 16 | queryClient.invalidateQueries({ queryKey: ["session"] }); 17 | }, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/statistics/types/index.ts: -------------------------------------------------------------------------------- 1 | // Re-export types from shared 2 | export type { 3 | WeightProgressionPoint, 4 | WeightProgressionResponse, 5 | OneRepMaxPoint, 6 | OneRepMaxResponse, 7 | VolumePoint, 8 | VolumeResponse, 9 | ExerciseStatisticsResponse, 10 | StatisticsErrorResponse, 11 | StatisticsRequestParams, 12 | } from "@/shared/types/statistics.types"; 13 | 14 | // Client-side specific types 15 | export interface ChartDimensions { 16 | width: number; 17 | height: number; 18 | padding: { 19 | top: number; 20 | right: number; 21 | bottom: number; 22 | left: number; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/features/workout-session/model/use-workout-sessions.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useQuery } from "@tanstack/react-query"; 4 | 5 | import { useWorkoutSessionService } from "@/shared/lib/workout-session/use-workout-session.service"; 6 | import { useSession } from "@/features/auth/lib/auth-client"; 7 | 8 | export function useWorkoutSessions() { 9 | const { data: session } = useSession(); 10 | 11 | const { getAll } = useWorkoutSessionService(); 12 | 13 | return useQuery({ 14 | queryKey: ["workout-sessions", session?.user?.id], 15 | queryFn: async () => { 16 | return getAll(); 17 | }, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/features/workout-session/ui/workout-sessions-synchronizer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { useSearchParams } from "next/navigation"; 5 | 6 | import { useSyncWorkoutSessions } from "../model/use-sync-workout-sessions"; 7 | 8 | export const WorkoutSessionsSynchronizer = () => { 9 | const { syncSessions } = useSyncWorkoutSessions(); 10 | const searchParams = useSearchParams(); 11 | const isSigninParam = searchParams.get("signin") === "true"; 12 | 13 | useEffect(() => { 14 | if (isSigninParam) { 15 | syncSessions(); 16 | } 17 | }, [isSigninParam]); 18 | 19 | return null; 20 | }; 21 | -------------------------------------------------------------------------------- /src/features/auth/ui/AuthButtonServer.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpButton } from "@/features/auth/ui/SignUpButton"; 2 | import { SignInButton } from "@/features/auth/ui/SignInButton"; 3 | import { LoggedInButton } from "@/features/auth/ui/LoggedInButton"; 4 | import { serverAuth } from "@/entities/user/model/get-server-session-user"; 5 | 6 | export const AuthButtonServer = async () => { 7 | const user = await serverAuth(); 8 | 9 | if (user) { 10 | return ; 11 | } 12 | 13 | return ( 14 |
15 | 16 | 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /app/[locale]/(app)/tools/calorie-calculator/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | // Export all shared components 2 | export { GenderSelector } from "./GenderSelector"; 3 | export { WeightInput } from "./WeightInput"; 4 | export { HeightInput } from "./HeightInput"; 5 | export { AgeInput } from "./AgeInput"; 6 | export { UnitSelector } from "./UnitSelector"; 7 | export { ActivityLevelSelector } from "./ActivityLevelSelector"; 8 | export { GoalSelector } from "./GoalSelector"; 9 | export { FAQSection } from "./FAQSection"; 10 | export { InfoButton } from "./InfoButton"; 11 | export { InfoModal } from "./InfoModal"; 12 | export { ResultsDisplay } from "./ResultsDisplay"; 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:15 4 | ports: 5 | - "${DB_PORT:-5432}:5432" 6 | volumes: 7 | - pgdata:/var/lib/postgresql/data 8 | healthcheck: 9 | test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] 10 | interval: 5s 11 | timeout: 5s 12 | retries: 5 13 | env_file: .env 14 | 15 | workout_cool: 16 | build: 17 | context: . 18 | dockerfile: Dockerfile 19 | ports: 20 | - "${APP_PORT:-3000}:3000" 21 | depends_on: 22 | postgres: 23 | condition: service_healthy 24 | env_file: .env 25 | volumes: 26 | pgdata: 27 | -------------------------------------------------------------------------------- /src/features/layout/useSidebarToggle.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useState } from "react"; 4 | 5 | export const useSidebarToggle = () => { 6 | const [isOpen, setIsOpen] = useState(false); 7 | 8 | const toggleSidebar = useCallback(() => { 9 | const sidebar = document.getElementById("sidebar"); 10 | const overlay = document.getElementById("overlay"); 11 | 12 | if (sidebar) { 13 | sidebar.classList.toggle("open"); 14 | } 15 | 16 | if (overlay) { 17 | overlay.classList.toggle("open"); 18 | } 19 | 20 | setIsOpen((prev) => !prev); 21 | }, []); 22 | 23 | return { isOpen, toggleSidebar }; 24 | }; 25 | -------------------------------------------------------------------------------- /app/[locale]/(admin)/admin/programs/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | 3 | import { ProgramBuilder } from "@/features/admin/programs/ui/program-builder"; 4 | import { getProgramById } from "@/features/admin/programs/actions/get-programs.action"; 5 | 6 | interface ProgramEditPageProps { 7 | params: Promise<{ id: string }>; 8 | } 9 | 10 | export default async function ProgramEditPage({ params }: ProgramEditPageProps) { 11 | const { id } = await params; 12 | 13 | const program = await getProgramById(id); 14 | 15 | if (!program) { 16 | notFound(); 17 | } 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ads/VerticalLeftBanner.tsx: -------------------------------------------------------------------------------- 1 | import { env } from "@/env"; 2 | 3 | import { VerticalAdBanner } from "./VerticalAdBanner"; 4 | 5 | export function VerticalLeftBanner() { 6 | const hasAdSlot = env.NEXT_PUBLIC_VERTICAL_LEFT_BANNER_AD_SLOT; 7 | const hasEzoicPlacement = env.NEXT_PUBLIC_EZOIC_VERTICAL_LEFT_PLACEMENT_ID; 8 | 9 | if (!hasAdSlot && !hasEzoicPlacement) { 10 | return null; 11 | } 12 | 13 | return ( 14 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { cn } from "@/shared/lib/utils"; 4 | 5 | interface SkeletonProps extends React.HTMLAttributes { 6 | width?: string | number; 7 | height?: string | number; 8 | rounded?: string; 9 | } 10 | 11 | export function Skeleton({ width, height, rounded = "rounded", className, ...props }: SkeletonProps) { 12 | return ( 13 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/pwa/ServiceWorkerRegistration.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export function ServiceWorkerRegistration() { 6 | useEffect(() => { 7 | if ("serviceWorker" in navigator) { 8 | navigator.serviceWorker 9 | .register("/sw.js") 10 | .then((registration) => { 11 | console.log("SW registered: ", registration); 12 | // Check for updates 13 | registration.update(); 14 | }) 15 | .catch((registrationError) => { 16 | console.log("SW registration failed: ", registrationError); 17 | }); 18 | } 19 | }, []); 20 | 21 | return null; 22 | } -------------------------------------------------------------------------------- /src/components/ads/VerticalRightBanner.tsx: -------------------------------------------------------------------------------- 1 | import { env } from "@/env"; 2 | 3 | import { VerticalAdBanner } from "./VerticalAdBanner"; 4 | 5 | export function VerticalRightBanner() { 6 | const hasAdSlot = env.NEXT_PUBLIC_VERTICAL_RIGHT_BANNER_AD_SLOT; 7 | const hasEzoicPlacement = env.NEXT_PUBLIC_EZOIC_VERTICAL_RIGHT_PLACEMENT_ID; 8 | 9 | if (!hasAdSlot && !hasEzoicPlacement) { 10 | return null; 11 | } 12 | 13 | return ( 14 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ui/divider.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/shared/lib/utils"; 2 | 3 | import type { ComponentProps } from "react"; 4 | 5 | export type DividerProps = ComponentProps<"div">; 6 | 7 | export const Divider = ({ className, children, ...props }: DividerProps) => { 8 | return ( 9 | 10 |
11 | 12 | {children} 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always 10 | frustrated when [...] 11 | 12 | **Describe the solution you'd like** A clear and concise description of what you want to happen. 13 | 14 | **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /src/components/utils/TailwindIndicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") return null; 3 | 4 | return ( 5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/[locale]/(app)/(legal-and-payment)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutParams } from "@/shared/types/next"; 2 | 3 | type LocaleParams = Record & { 4 | locale: string; 5 | }; 6 | 7 | export default function RouteLayout({ children, params: _ }: LayoutParams) { 8 | return ( 9 |
10 | {/* Fixe l'espace sous le header flottant */} 11 |
12 | 13 | {/* Contenu principal centré avec marge */} 14 |
15 |
{children}
16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Running Prisma migrations..." 4 | npx prisma migrate deploy 5 | 6 | echo "Generating Prisma client..." 7 | npx prisma generate 8 | 9 | if [ "$SEED_SAMPLE_DATA" = "true" ]; then 10 | echo "Seed sample data enabled, importing sample data..." 11 | # Import exercises if CSV exists 12 | if [ -f "./data/sample-exercises.csv" ]; then 13 | npx tsx scripts/import-exercises-with-attributes.ts ./data/sample-exercises.csv 14 | else 15 | echo "No exercises sample data found, skipping import." 16 | fi 17 | else 18 | echo "Skipping sample data import." 19 | fi 20 | 21 | echo "Starting the app..." 22 | exec "$@" # runs the CMD from the Dockerfile 23 | -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/verify-email/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import { getServerUrl } from "@/shared/lib/server-url"; 5 | import { paths } from "@/shared/constants/paths"; 6 | import { serverRequiredUser } from "@/entities/user/model/get-server-session-user"; 7 | 8 | interface RootLayoutProps { 9 | params: Promise<{ locale: string }>; 10 | children: ReactElement; 11 | } 12 | 13 | export default async function RootLayout({ children }: RootLayoutProps) { 14 | const auth = await serverRequiredUser(); 15 | 16 | if (auth.emailVerified) { 17 | redirect(`${getServerUrl()}/${paths.root}`); 18 | } 19 | 20 | return children; 21 | } 22 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Disallow: /admin/ 4 | Disallow: /api/ 5 | Disallow: /dashboard/ 6 | Disallow: /preview/ 7 | Disallow: /auth/ 8 | Disallow: /onboarding/ 9 | Disallow: /profile/ 10 | Disallow: /_next/ 11 | Disallow: /.*\? 12 | 13 | # Crawl delay 14 | Crawl-delay: 1 15 | 16 | # Specific bot configurations 17 | User-agent: Googlebot 18 | Allow: / 19 | Disallow: /admin/ 20 | Disallow: /api/ 21 | Disallow: /auth/ 22 | Disallow: /onboarding/ 23 | Disallow: /profile/ 24 | 25 | User-agent: Bingbot 26 | Allow: / 27 | Disallow: /admin/ 28 | Disallow: /api/ 29 | Disallow: /auth/ 30 | Disallow: /onboarding/ 31 | Disallow: /profile/ 32 | 33 | # Sitemap 34 | Sitemap: https://www.workout.cool/sitemap.xml 35 | -------------------------------------------------------------------------------- /src/features/admin/programs/ui/create-program-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Plus } from "lucide-react"; 5 | 6 | import { CreateProgramModal } from "./create-program-modal"; 7 | 8 | export function CreateProgramButton() { 9 | const [isModalOpen, setIsModalOpen] = useState(false); 10 | 11 | return ( 12 | <> 13 | 20 | 21 | 25 | 26 | ); 27 | } -------------------------------------------------------------------------------- /app/api/programs/[slug]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { getProgramBySlug } from "@/features/programs/actions/get-program-by-slug.action"; 4 | 5 | export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) { 6 | try { 7 | const { slug } = await params; 8 | const program = await getProgramBySlug(slug); 9 | 10 | if (!program) { 11 | return NextResponse.json({ error: "Program not found" }, { status: 404 }); 12 | } 13 | 14 | return NextResponse.json(program); 15 | } catch (error) { 16 | console.error("Error fetching program:", error); 17 | return NextResponse.json({ error: "Failed to fetch program" }, { status: 500 }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entities/user/model/get-server-session-user.ts: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import { headers } from "next/headers"; 3 | 4 | import { auth } from "@/features/auth/lib/better-auth"; 5 | export class AuthError extends Error { 6 | constructor(message: string) { 7 | super(message); 8 | } 9 | } 10 | 11 | export const serverAuth = async () => { 12 | const session = await auth.api.getSession({ headers: await headers() }); 13 | 14 | if (session && session.user) { 15 | return session.user; 16 | } 17 | 18 | return null; 19 | }; 20 | 21 | export const serverRequiredUser = async () => { 22 | const user = await serverAuth(); 23 | 24 | if (!user) { 25 | notFound(); 26 | } 27 | 28 | return user; 29 | }; 30 | -------------------------------------------------------------------------------- /app/[locale]/(app)/tools/heart-rate-zones/ui/components/ScrollToTopButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { useScrollToTop } from "@/shared/hooks/useScrollToTop"; 6 | 7 | interface ScrollToTopButtonProps { 8 | text: string; 9 | } 10 | 11 | export function ScrollToTopButton({ text }: ScrollToTopButtonProps) { 12 | const scrollToTop = useScrollToTop(); 13 | 14 | return ( 15 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/hooks/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useCallback, useState } from "react"; 2 | 3 | export interface UseBooleanReturn { 4 | value: boolean; 5 | setValue: Dispatch>; 6 | setTrue: () => void; 7 | setFalse: () => void; 8 | toggle: () => void; 9 | } 10 | 11 | function useBoolean(defaultValue?: boolean): UseBooleanReturn { 12 | const [value, setValue] = useState(Boolean(defaultValue)); 13 | 14 | const setTrue = useCallback(() => setValue(true), []); 15 | const setFalse = useCallback(() => setValue(false), []); 16 | const toggle = useCallback(() => setValue((prev) => !prev), []); 17 | 18 | return { value, setValue, setTrue, setFalse, toggle }; 19 | } 20 | 21 | export default useBoolean; 22 | -------------------------------------------------------------------------------- /src/shared/lib/mdx/load-mdx.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { readFile } from "fs/promises"; 3 | 4 | import { compileMDX } from "next-mdx-remote/rsc"; 5 | 6 | import { WorkoutLol } from "@/components/ui/workout-lol"; 7 | 8 | export async function getLocalizedMdx( 9 | pageSlug: string, // ex: "privacy-policy" 10 | locale: string, // ex: "fr" or "en" 11 | ) { 12 | const filePath = path.join(process.cwd(), "content", pageSlug, `${locale}.mdx`); 13 | 14 | const source = await readFile(filePath, "utf-8"); 15 | 16 | const { content } = await compileMDX({ 17 | source, 18 | options: { 19 | parseFrontmatter: true, 20 | }, 21 | components: { 22 | WorkoutLol, 23 | }, 24 | }); 25 | 26 | return content; 27 | } 28 | -------------------------------------------------------------------------------- /src/shared/lib/network/use-network-status.ts: -------------------------------------------------------------------------------- 1 | // src/shared/lib/network/useNetworkStatus.ts 2 | import { useEffect, useState } from "react"; 3 | 4 | export function useNetworkStatus() { 5 | const [isOnline, setIsOnline] = useState(typeof window !== "undefined" ? navigator.onLine : true); 6 | 7 | useEffect(() => { 8 | const handleOnline = () => setIsOnline(true); 9 | const handleOffline = () => setIsOnline(false); 10 | window.addEventListener("online", handleOnline); 11 | window.addEventListener("offline", handleOffline); 12 | return () => { 13 | window.removeEventListener("online", handleOnline); 14 | window.removeEventListener("offline", handleOffline); 15 | }; 16 | }, []); 17 | 18 | return { isOnline }; 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/lib/server-url.ts: -------------------------------------------------------------------------------- 1 | import { SiteConfig } from "@/shared/config/site-config"; 2 | 3 | /** 4 | * This method return the server URL based on the environment. 5 | */ 6 | export const getServerUrl = () => { 7 | if (typeof window !== "undefined") { 8 | return window.location.origin; 9 | } 10 | 11 | // If we are in production, we return the production URL. 12 | if (process.env.VERCEL_ENV === "production") { 13 | return SiteConfig.prodUrl; 14 | } 15 | 16 | // If we are in "stage" environment, we return the staging URL. 17 | if (process.env.VERCEL_URL) { 18 | return `https://${process.env.VERCEL_URL}`; 19 | } 20 | 21 | // If we are in development, we return the localhost URL. 22 | return "http://localhost:3000"; 23 | }; 24 | -------------------------------------------------------------------------------- /src/shared/lib/workout-session/workout-session.api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import type { WorkoutSession } from "./types/workout-session"; 3 | 4 | export const workoutSessionApi = { 5 | getAll: async (): Promise => { 6 | // TODO: fetch("/api/workout-sessions") 7 | return []; 8 | }, 9 | create: async (session: WorkoutSession): Promise<{ id: string }> => { 10 | // TODO: POST /api/workout-sessions 11 | return { id: "server-uuid" }; 12 | }, 13 | update: async (id: string, data: Partial) => { 14 | // TODO: PATCH /api/workout-sessions/:id 15 | }, 16 | complete: async (id: string) => { 17 | // TODO: PATCH /api/workout-sessions/:id/complete 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/svg/DotPattern.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/shared/lib/utils"; 2 | 3 | import type { ComponentPropsWithoutRef } from "react"; 4 | 5 | export type DotPatternProps = ComponentPropsWithoutRef<"div">; 6 | 7 | export const DotPattern = ({ children, className, ...props }: DotPatternProps) => { 8 | return ( 9 |
10 |
18 |
{children}
19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/shared/hooks/useIsMobile.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | export function useIsMobile(breakpoint: number = 768) { 6 | const [isMobile, setIsMobile] = useState(false); 7 | 8 | useEffect(() => { 9 | // Check if window is defined (client-side) 10 | if (typeof window === "undefined") return; 11 | 12 | const checkIsMobile = () => { 13 | setIsMobile(window.innerWidth < breakpoint); 14 | }; 15 | 16 | // Initial check 17 | checkIsMobile(); 18 | 19 | // Add event listener 20 | window.addEventListener("resize", checkIsMobile); 21 | 22 | // Cleanup 23 | return () => window.removeEventListener("resize", checkIsMobile); 24 | }, [breakpoint]); 25 | 26 | return isMobile; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | 5 | import { cn } from "@/shared/lib/utils"; 6 | 7 | const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"); 8 | 9 | const Label = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef & VariantProps 12 | >(({ className, ...props }, ref) => ); 13 | Label.displayName = LabelPrimitive.Root.displayName; 14 | 15 | export { Label }; 16 | -------------------------------------------------------------------------------- /src/features/workout-session/types/workout-set.ts: -------------------------------------------------------------------------------- 1 | import { ExerciseWithAttributes } from "@/entities/exercise/types/exercise.types"; 2 | 3 | export type WorkoutSetType = "TIME" | "WEIGHT" | "REPS" | "BODYWEIGHT" | "NA"; 4 | export type WorkoutSetUnit = "kg" | "lbs"; 5 | 6 | export interface WorkoutSet { 7 | id: string; 8 | setIndex: number; 9 | types: WorkoutSetType[]; // To support multiple columns 10 | valuesInt?: number[]; // To support multiple columns 11 | valuesSec?: number[]; // To support multiple columns 12 | units?: WorkoutSetUnit[]; // Pour supporter plusieurs colonnes 13 | completed: boolean; 14 | } 15 | 16 | export interface WorkoutSessionExercise extends ExerciseWithAttributes { 17 | id: string; 18 | order: number; 19 | sets: WorkoutSet[]; 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/lib/analytics/events.ts: -------------------------------------------------------------------------------- 1 | export const LogEvents = { 2 | Registered: { 3 | name: "User Registered", 4 | channel: "registered", 5 | }, 6 | EnrolledInProgram: { 7 | name: "User Enrolled in Program", 8 | channel: "program", 9 | }, 10 | PremiumDiscovery: { 11 | name: "Premium Features Discovered", 12 | channel: "premium", 13 | }, 14 | PaywallViewed: { 15 | name: "Paywall Viewed", 16 | channel: "premium", 17 | }, 18 | PaywallPurchased: { 19 | name: "Paywall Purchase Completed", 20 | channel: "premium", 21 | }, 22 | PaywallCancelled: { 23 | name: "Paywall Cancelled", 24 | channel: "premium", 25 | }, 26 | PaywallRestored: { 27 | name: "Purchases Restored", 28 | channel: "premium", 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250505191954_admin_and_user_lowercase/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The values [ADMIN,USER] on the enum `UserRole` will be removed. If these variants are still used in the database, this will fail. 5 | 6 | */ 7 | -- AlterEnum 8 | BEGIN; 9 | CREATE TYPE "UserRole_new" AS ENUM ('admin', 'user'); 10 | ALTER TABLE "user" ALTER COLUMN "role" DROP DEFAULT; 11 | ALTER TABLE "user" ALTER COLUMN "role" TYPE "UserRole_new" USING ("role"::text::"UserRole_new"); 12 | ALTER TYPE "UserRole" RENAME TO "UserRole_old"; 13 | ALTER TYPE "UserRole_new" RENAME TO "UserRole"; 14 | DROP TYPE "UserRole_old"; 15 | ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'user'; 16 | COMMIT; 17 | 18 | -- AlterTable 19 | ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'user'; 20 | -------------------------------------------------------------------------------- /src/components/svg/VerifiedBadge.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from "react"; 2 | 3 | export const VerifiedBadge = (props: ComponentPropsWithoutRef<"svg">) => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/auth/reset-password/schema/reset-password.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const resetPasswordSchema = z 4 | .object({ 5 | password: z 6 | .string() 7 | .min(8, "Le mot de passe doit contenir au moins 8 caractères") 8 | .regex(/[A-Z]/, "Le mot de passe doit contenir au moins une majuscule") 9 | .regex(/[a-z]/, "Le mot de passe doit contenir au moins une minuscule") 10 | .regex(/[0-9]/, "Le mot de passe doit contenir au moins un chiffre"), 11 | confirmPassword: z.string(), 12 | }) 13 | .refine((data) => data.password === data.confirmPassword, { 14 | message: "Les mots de passe ne correspondent pas", 15 | path: ["confirmPassword"], 16 | }); 17 | 18 | export type ResetPasswordFormData = z.infer; 19 | -------------------------------------------------------------------------------- /src/shared/lib/workout-session/types/workout-session.ts: -------------------------------------------------------------------------------- 1 | import { ExerciseAttributeValueEnum } from "@prisma/client"; 2 | 3 | import { WorkoutSessionExercise } from "@/features/workout-session/types/workout-set"; 4 | 5 | export const workoutSessionStatuses = ["active", "completed", "synced"] as const; 6 | export type WorkoutSessionStatus = (typeof workoutSessionStatuses)[number]; 7 | 8 | export interface WorkoutSession { 9 | id: string; // local: "local-xxx", server: uuid 10 | userId: string; 11 | status?: WorkoutSessionStatus; 12 | startedAt: string; 13 | endedAt?: string; 14 | duration?: number; 15 | exercises: WorkoutSessionExercise[]; 16 | currentExerciseIndex?: number; 17 | isActive?: boolean; 18 | serverId?: string; // If synced 19 | muscles: ExerciseAttributeValueEnum[]; 20 | } 21 | -------------------------------------------------------------------------------- /app/api/programs/[slug]/progress/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { getProgramProgressBySlug } from "@/features/programs/actions/get-program-progress-by-slug.action"; 4 | 5 | export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) { 6 | try { 7 | const { slug } = await params; 8 | 9 | const progress = await getProgramProgressBySlug(slug); 10 | 11 | if (!progress) { 12 | return NextResponse.json({ error: "Program progress not found" }, { status: 404 }); 13 | } 14 | 15 | return NextResponse.json(progress); 16 | } catch (error) { 17 | console.error("Error fetching program progress:", error); 18 | return NextResponse.json({ error: "Failed to fetch program progress" }, { status: 500 }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250615170916_add_cascade_delete_workout_sessions/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "workout_session_exercises" DROP CONSTRAINT "workout_session_exercises_workoutSessionId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "workout_sets" DROP CONSTRAINT "workout_sets_workoutSessionExerciseId_fkey"; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "workout_session_exercises" ADD CONSTRAINT "workout_session_exercises_workoutSessionId_fkey" FOREIGN KEY ("workoutSessionId") REFERENCES "workout_sessions"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "workout_sets" ADD CONSTRAINT "workout_sets_workoutSessionExerciseId_fkey" FOREIGN KEY ("workoutSessionExerciseId") REFERENCES "workout_session_exercises"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /src/shared/lib/location/eu-countries.ts: -------------------------------------------------------------------------------- 1 | export const EU_COUNTRY_CODES = [ 2 | "AT", 3 | "BE", 4 | "BG", 5 | "HR", 6 | "CY", 7 | "CZ", 8 | "DK", 9 | "EE", 10 | "FI", 11 | "FR", 12 | "DE", 13 | "GR", 14 | "HU", 15 | "IE", 16 | "IT", 17 | "LV", 18 | "LT", 19 | "LU", 20 | "MT", 21 | "NL", 22 | "PL", 23 | "PT", 24 | "RO", 25 | "SK", 26 | "SI", 27 | "ES", 28 | "SE", 29 | "GB", 30 | "GI", 31 | "IS", 32 | "LI", 33 | "NO", 34 | "CH", 35 | "ME", 36 | "MK", 37 | "RS", 38 | "TR", 39 | "AL", 40 | "BA", 41 | "XK", 42 | "AD", 43 | "BY", 44 | "MD", 45 | "MC", 46 | "RU", 47 | "UA", 48 | "VA", 49 | "AX", 50 | "FO", 51 | "GL", 52 | "SJ", 53 | "IM", 54 | "JE", 55 | "GG", 56 | "RS", 57 | "ME", 58 | "XK", 59 | "RS", 60 | ]; 61 | -------------------------------------------------------------------------------- /src/features/consent-banner/model/tracking-consent.action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | import { Cookies } from "@/shared/constants/cookies"; 6 | import { actionClient } from "@/shared/api/safe-actions"; 7 | import { trackingConsentSchema } from "@/features/consent-banner/schema/tracking-consent.schema"; 8 | 9 | export const trackingConsentAction = actionClient.schema(trackingConsentSchema).action(async ({ parsedInput: value }) => { 10 | const cookiesStore = await cookies(); 11 | 12 | const oneYearFromNow = new Date(); 13 | oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); 14 | 15 | cookiesStore.set({ 16 | name: Cookies.TrackingConsent, 17 | value: value ? "1" : "0", 18 | expires: oneYearFromNow, 19 | }); 20 | 21 | return value; 22 | }); 23 | -------------------------------------------------------------------------------- /src/shared/lib/premium/use-premium-redirect.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { useRouter, useSearchParams } from "next/navigation"; 5 | 6 | import { useIsPremium } from "./use-premium"; 7 | 8 | export function usePremiumRedirect() { 9 | const router = useRouter(); 10 | const searchParams = useSearchParams(); 11 | const isPremium = useIsPremium(); 12 | 13 | useEffect(() => { 14 | // Check if user just became premium and has a return URL 15 | if (isPremium) { 16 | const returnUrl = searchParams.get("return"); 17 | if (returnUrl) { 18 | // Small delay to ensure premium status is fully updated 19 | setTimeout(() => { 20 | router.push(returnUrl); 21 | }, 1000); 22 | } 23 | } 24 | }, [isPremium, searchParams, router]); 25 | } -------------------------------------------------------------------------------- /app/[locale]/(admin)/admin/programs/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | 3 | import { ProgramsList } from "@/features/admin/programs/ui/programs-list"; 4 | import { CreateProgramButton } from "@/features/admin/programs/ui/create-program-button"; 5 | 6 | export default function AdminPrograms() { 7 | return ( 8 |
9 |
10 |
11 |

Programs

12 |

Create, edit, view and delete programs.

13 |
14 | 15 |
16 | 17 | Loading programs...
}> 18 | 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/shared/lib/utils"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( 12 | 19 | )); 20 | Separator.displayName = SeparatorPrimitive.Root.displayName; 21 | 22 | export { Separator }; 23 | -------------------------------------------------------------------------------- /src/components/ui/star-button.tsx: -------------------------------------------------------------------------------- 1 | import { Star } from "lucide-react"; 2 | 3 | import { cn } from "@/shared/lib/utils"; 4 | 5 | interface StarButtonProps { 6 | isActive: boolean; 7 | isLoading: boolean; 8 | onClick?: (e: React.MouseEvent) => void; 9 | className?: string; 10 | children?: React.ReactNode; 11 | } 12 | 13 | export function StarButton({ isActive, isLoading, onClick, className, children }: StarButtonProps) { 14 | return ( 15 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | import { logger } from "@/shared/lib/logger"; 6 | import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 7 | import { Button } from "@/components/ui/button"; 8 | 9 | import type { ErrorParams } from "@/shared/types/next"; 10 | 11 | export default function RouteError({ error, reset }: ErrorParams) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | logger.error(error); 15 | }, [error]); 16 | 17 | return ( 18 | 19 | 20 | Sorry, something went wrong. Please try again later. 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/features/auth/lib/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from "better-auth/react"; 2 | import { adminClient, customSessionClient, inferAdditionalFields } from "better-auth/client/plugins"; 3 | 4 | import { getServerUrl } from "@/shared/lib/server-url"; 5 | import { auth } from "@/features/auth/lib/better-auth"; 6 | 7 | export const authClient = createAuthClient({ 8 | /** The base URL of the server (optional if you're using the same domain) */ 9 | baseURL: getServerUrl(), 10 | plugins: [adminClient(), customSessionClient(), inferAdditionalFields()], 11 | }); 12 | 13 | export const useIsAdmin = () => { 14 | const { data: sessionData, isPending: isSessionLoading } = useSession(); 15 | return !isSessionLoading && sessionData?.user?.role?.includes("admin"); 16 | }; 17 | 18 | export const { useSession } = authClient; 19 | -------------------------------------------------------------------------------- /src/features/statistics/hooks/use-chart-theme.ts: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes"; 2 | 3 | export function useChartTheme() { 4 | const { theme, systemTheme } = useTheme(); 5 | const currentTheme = theme === "system" ? systemTheme : theme; 6 | const isDark = currentTheme === "dark"; 7 | 8 | return { 9 | isDark, 10 | colors: { 11 | background: isDark ? "#1f2937" : "#ffffff", 12 | cardBackground: isDark ? "#374151" : "#ffffff", 13 | text: isDark ? "#f9fafb" : "#374151", 14 | textSecondary: isDark ? "#d1d5db" : "#6b7280", 15 | textMuted: isDark ? "#9ca3af" : "#6b7280", 16 | border: isDark ? "#4b5563" : "#e5e7eb", 17 | grid: isDark ? "#4b5563" : "#e5e7eb", 18 | tooltipBackground: isDark ? "#374151" : "#ffffff", 19 | tooltipBorder: isDark ? "#4b5563" : "#e5e7eb", 20 | }, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/features/workout-builder/ui/muscles.module.css: -------------------------------------------------------------------------------- 1 | .svgContainer { 2 | text-align: center; 3 | margin: 2em 0; 4 | position: relative; 5 | } 6 | 7 | .illustration { 8 | width: 100%; 9 | } 10 | 11 | .illustration path { 12 | position: relative; 13 | z-index: 0; 14 | } 15 | 16 | .muscle { 17 | } 18 | .muscleContainer { 19 | z-index: 1; 20 | cursor: pointer; 21 | } 22 | 23 | .muscle.enabled.hover { 24 | fill: #69b0ee; 25 | cursor: pointer; 26 | } 27 | 28 | .muscle.enabled.active { 29 | fill: #228be6 !important; 30 | } 31 | 32 | .muscle.enabled { 33 | fill: #bdbdbd; 34 | } 35 | 36 | .loading { 37 | fill: #757575; 38 | animation: pulse 2s linear infinite; 39 | } 40 | 41 | @keyframes pulse { 42 | 0% { 43 | opacity: 0.5; 44 | } 45 | 50% { 46 | opacity: 0.8; 47 | } 48 | 100% { 49 | opacity: 0.5; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/[locale]/(app)/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { buttonVariants } from "@/components/ui/button"; 5 | 6 | export default async function AuthErrorPage({ params }: { params: Promise<{ error: string }> }) { 7 | const result = await params; 8 | 9 | return ( 10 |
11 | 12 | 13 | Error 14 | {result.error} 15 | 16 | 17 | 18 | Home 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/features/leaderboard/ui/leaderboard-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | 3 | const LeaderboardSkeleton: React.FC = () => ( 4 |
5 | {[...Array(5)].map((_, i) => ( 6 |
7 | 8 | 9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | ))} 19 |
20 | ); 21 | 22 | export default LeaderboardSkeleton; 23 | -------------------------------------------------------------------------------- /src/features/leaderboard/hooks/use-top-workout-users.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useQuery } from "@tanstack/react-query"; 4 | 5 | import { getTopWorkoutUsersAction, LeaderboardPeriod } from "../actions/get-top-workout-users.action"; 6 | 7 | export interface UseTopWorkoutUsersOptions { 8 | refetchInterval?: number; 9 | period?: LeaderboardPeriod; 10 | } 11 | 12 | export function useTopWorkoutUsers(options: UseTopWorkoutUsersOptions = {}) { 13 | const { refetchInterval, period = "all-time" } = options; 14 | 15 | return useQuery({ 16 | queryKey: ["top-workout-users", period], 17 | queryFn: async () => { 18 | const result = await getTopWorkoutUsersAction({ period }); 19 | return result?.data || []; 20 | }, 21 | staleTime: 5 * 60 * 1000, // 5 minutes 22 | refetchInterval, 23 | refetchOnWindowFocus: false, 24 | retry: 3, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /_next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # env files (can opt-in for committing if needed) 35 | .env 36 | .env.local 37 | .env.test 38 | .env.development 39 | .env.production 40 | 41 | # vercel 42 | .vercel 43 | 44 | # typescript 45 | *.tsbuildinfo 46 | next-env.d.ts 47 | 48 | # editors 49 | .idea 50 | .zed 51 | scripts/private/ 52 | scripts/personal/ 53 | product-development/ 54 | .claude/ 55 | .cache/ 56 | -------------------------------------------------------------------------------- /src/components/premium/RemoveAdsText.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { Ban } from "lucide-react"; 5 | 6 | import { useI18n } from "locales/client"; 7 | 8 | export function RemoveAdsText() { 9 | const router = useRouter(); 10 | const t = useI18n(); 11 | 12 | const handleClick = () => { 13 | router.push("/premium"); 14 | }; 15 | 16 | return ( 17 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/features/theme/ui/ThemeSynchronizer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | /** 7 | * Synchronizes the tag with the current theme (light/dark). 8 | * Ensures the browser UI (mobile address bar, etc.) matches the user's selected theme. 9 | */ 10 | export function ThemeSynchronizer() { 11 | const { resolvedTheme } = useTheme(); 12 | 13 | useEffect(() => { 14 | const themeColor = resolvedTheme === "dark" ? "#18181b" : "#f3f4f6"; 15 | let meta = document.querySelector("meta[name=theme-color]"); 16 | if (!meta) { 17 | meta = document.createElement("meta"); 18 | meta.setAttribute("name", "theme-color"); 19 | document.head.appendChild(meta); 20 | } 21 | meta.setAttribute("content", themeColor); 22 | }, [resolvedTheme]); 23 | 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /src/features/workout-builder/ui/favorite-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/shared/lib/utils"; 4 | import { StarButton } from "@/components/ui/star-button"; 5 | 6 | interface FavoriteButtonProps { 7 | exerciseId: string; 8 | isFavorite: boolean; 9 | onToggle: (exerciseId: string) => void; 10 | className?: string; 11 | } 12 | 13 | export const FavoriteButton = ({ exerciseId, isFavorite, onToggle, className = "" }: FavoriteButtonProps) => { 14 | const handleToggle = (e: React.MouseEvent) => { 15 | e.stopPropagation(); 16 | 17 | try { 18 | onToggle(exerciseId); 19 | } catch (error) { 20 | console.error("Failed to toggle favorite:", error); 21 | } 22 | }; 23 | 24 | return ( 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /app/[locale]/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | 3 | import { Header } from "@/features/layout/Header"; 4 | import { Footer } from "@/features/layout/Footer"; 5 | import { BottomNavigation } from "@/features/layout/BottomNavigation"; 6 | 7 | interface RootLayoutProps { 8 | params: Promise<{ locale: string }>; 9 | children: ReactElement; 10 | } 11 | 12 | export default async function RootLayout({ children }: RootLayoutProps) { 13 | return ( 14 |
15 |
16 |
{children}
17 | 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250414174246_rename_feedbacks/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Feedback` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Feedback" DROP CONSTRAINT "Feedback_userId_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "Feedback"; 12 | 13 | -- CreateTable 14 | CREATE TABLE "feedbacks" ( 15 | "id" TEXT NOT NULL, 16 | "review" INTEGER NOT NULL, 17 | "message" TEXT NOT NULL, 18 | "email" TEXT, 19 | "userId" TEXT, 20 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | "updatedAt" TIMESTAMP(3) NOT NULL, 22 | 23 | CONSTRAINT "feedbacks_pkey" PRIMARY KEY ("id") 24 | ); 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "feedbacks" ADD CONSTRAINT "feedbacks_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE; 28 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250707114920_add_user_favorite_exercises/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "user_favorite_exercises" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "exerciseId" TEXT NOT NULL, 6 | "updatedAt" TIMESTAMP(3) NOT NULL, 7 | 8 | CONSTRAINT "user_favorite_exercises_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "user_favorite_exercises_userId_exerciseId_key" ON "user_favorite_exercises"("userId", "exerciseId"); 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "user_favorite_exercises" ADD CONSTRAINT "user_favorite_exercises_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "user_favorite_exercises" ADD CONSTRAINT "user_favorite_exercises_exerciseId_fkey" FOREIGN KEY ("exerciseId") REFERENCES "exercises"("id") ON DELETE CASCADE ON UPDATE CASCADE; 19 | -------------------------------------------------------------------------------- /src/entities/user/lib/display-name.ts: -------------------------------------------------------------------------------- 1 | import { SessionUser } from "@/entities/user/types/session-user"; 2 | 3 | export function displayName(user: SessionUser): string { 4 | return user.name 5 | ? user.name 6 | : user.email 7 | .split("@")[0] 8 | .replaceAll(".", " ") 9 | .replace(/^\w/, (c) => c.toUpperCase()); 10 | } 11 | 12 | export function displayFullName({ firstName, lastName }: { firstName: string; lastName: string }): string { 13 | return `${firstName} ${lastName}`; 14 | } 15 | 16 | export function displayFirstNameAndFirstLetterLastName(user: SessionUser): string { 17 | return `${user.firstName} ${user.lastName?.charAt(0)}.`; 18 | } 19 | 20 | export const displayFirstName = (user: SessionUser): string => { 21 | return user.firstName; 22 | }; 23 | 24 | export const displayInitials = (user: SessionUser): string => { 25 | return user.firstName.charAt(0) + user.lastName.charAt(0); 26 | }; 27 | -------------------------------------------------------------------------------- /prisma/migrations_backup/20250117000000_add_statistics_indexes/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex for exercise statistics queries 2 | -- Index for workout_session_exercises by exerciseId and session date 3 | CREATE INDEX "workout_session_exercises_exerciseId_idx" ON "workout_session_exercises"("exerciseId"); 4 | 5 | -- Index for workout_sessions by userId and createdAt 6 | CREATE INDEX "workout_sessions_userId_createdAt_idx" ON "workout_sessions"("userId", "createdAt"); 7 | 8 | -- Index for workout_sets by completed status and types array 9 | CREATE INDEX "workout_sets_completed_idx" ON "workout_sets"("completed"); 10 | 11 | -- Composite index for efficient statistics queries 12 | CREATE INDEX "workout_sessions_userId_createdAt_desc_idx" ON "workout_sessions"("userId", "createdAt" DESC); 13 | 14 | -- Index for workoutSessionExerciseId lookups 15 | CREATE INDEX "workout_sets_workoutSessionExerciseId_idx" ON "workout_sets"("workoutSessionExerciseId"); -------------------------------------------------------------------------------- /src/features/leaderboard/hooks/use-user-position.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useQuery } from "@tanstack/react-query"; 4 | 5 | import { getUserPositionAction } from "../actions/get-user-position.action"; 6 | import { LeaderboardPeriod } from "../actions/get-top-workout-users.action"; 7 | 8 | interface UseUserPositionOptions { 9 | userId: string | undefined; 10 | period: LeaderboardPeriod; 11 | enabled?: boolean; 12 | } 13 | 14 | export function useUserPosition({ userId, period, enabled = true }: UseUserPositionOptions) { 15 | return useQuery({ 16 | queryKey: ["user-position", userId, period], 17 | queryFn: async () => { 18 | if (!userId) return null; 19 | const result = await getUserPositionAction({ userId, period }); 20 | return result?.data || null; 21 | }, 22 | enabled: enabled && !!userId, 23 | staleTime: 5 * 60 * 1000, // 5 minutes 24 | refetchOnWindowFocus: false, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/features/contact/support/contact-support.action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import ContactSupportEmail from "@emails/ContactSupportEmail"; 4 | import { sendEmail } from "@/shared/lib/mail/sendEmail"; 5 | import { SiteConfig } from "@/shared/config/site-config"; 6 | import { actionClient } from "@/shared/api/safe-actions"; 7 | 8 | import { ContactSupportSchema } from "./contact-support.schema"; 9 | 10 | export const contactSupportAction = actionClient.schema(ContactSupportSchema).action(async ({ parsedInput }) => { 11 | await sendEmail({ 12 | from: SiteConfig.email.from, 13 | to: SiteConfig.email.contact, 14 | subject: `Support needed from ${parsedInput.email} - ${parsedInput.subject}`, 15 | text: parsedInput.message, 16 | react: ContactSupportEmail({ email: parsedInput.email, message: parsedInput.message, subject: parsedInput.subject }), 17 | }); 18 | return { message: "Your message has been sent to support." }; 19 | }); 20 | -------------------------------------------------------------------------------- /src/features/theme/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { MoonIcon, SunIcon } from "lucide-react"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | 8 | export function ThemeToggle() { 9 | const { setTheme, resolvedTheme } = useTheme(); 10 | 11 | return ( 12 |
13 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/shared/lib/utils"; 4 | 5 | export interface TextareaProps extends React.TextareaHTMLAttributes {} 6 | 7 | const Textarea = React.forwardRef(({ className, ...props }, ref) => { 8 | return ( 9 |