├── .eslintrc.json ├── .vscode └── settings.json ├── postcss.config.js ├── utils ├── picture-utils.js ├── lazyload-config.js ├── get-stripe.js ├── enforce-api-route-secret.js ├── weight-utils.js ├── wii-balance-board │ ├── helpers.js │ ├── const.js │ └── WiiBalanceBoard.js ├── exercise-utils.js ├── send-email.js ├── subscription-utils.js ├── EventDispatcher.js ├── supabase.js ├── MiSmartScale2.js ├── MiBodyCompositionScale.js ├── MIBCSMetrics.js └── withings.js ├── public ├── favicon.ico ├── images │ ├── logo.png │ ├── icon-192x192.png │ ├── icon-256x256.png │ ├── icon-384x384.png │ ├── icon-512x512.png │ ├── icon-96x96.png │ ├── apple-touch-icon.png │ └── icon.svg ├── features │ ├── screenshot_1.png │ ├── screenshot_2.png │ ├── screenshot_3.png │ ├── screenshot_4.png │ ├── screenshot_5.png │ ├── screenshot_6.png │ └── screenshot_7.png ├── fallback-GtbOZAaAkPqSfUfS3cKdI.js ├── manifest-ios.json └── manifest.json ├── prettier.config.js ├── components ├── MyLink.jsx ├── dashboard │ ├── notification │ │ └── DeleteAccountNotification.jsx │ ├── modal │ │ ├── DeleteAccountModal.jsx │ │ ├── DeleteUserModal.jsx │ │ ├── DeleteSubscriptionModal.jsx │ │ ├── DeletePictureModal.jsx │ │ ├── DeleteWeightModal.jsx │ │ ├── DeleteExerciseTypeModal.jsx │ │ ├── DeleteExerciseModal.jsx │ │ └── DeleteBlockModal.jsx │ └── ClientsSelect.jsx ├── layouts │ ├── Layout.jsx │ └── DashboardLayout.jsx ├── GoogleDriveVideo.jsx ├── QRCodeModal.jsx ├── OfflineBanner.jsx ├── LazyImage.jsx ├── LazyVideo.jsx ├── Notification.jsx ├── ExerciseTypeVideo.jsx ├── Modal.jsx └── Footer.jsx ├── styles └── index.css ├── .gitignore ├── tailwind.config.js ├── next.config.js ├── context ├── progress-context.jsx ├── online-context.jsx ├── exercise-types-context.jsx ├── coach-picture-context.jsx ├── exercise-videos-context.jsx ├── selected-exercise-context.jsx ├── picture-context.jsx ├── wii-balance-board-context.jsx └── user-context.jsx ├── .env.local.example ├── pages ├── subscription │ └── [id].jsx ├── _offline.jsx ├── api │ ├── account │ │ ├── stripe-dashboard.js │ │ ├── stripe-customer-portal.js │ │ ├── stripe-onboarding.js │ │ ├── setup-account.js │ │ ├── set-withings-auth-code.js │ │ ├── refresh-withings-access-token.js │ │ ├── get-withings-access-token.js │ │ ├── set-notifications.js │ │ ├── check-stripe.js │ │ └── sign-in.js │ ├── webhook │ │ ├── stripe │ │ │ └── account.js │ │ └── withings │ │ │ └── data.js │ └── subscription │ │ ├── delete-subscription.js │ │ ├── redeem-subscription.js │ │ └── create-subscription.js ├── 404.jsx ├── terms.jsx ├── _document.jsx ├── _app.jsx ├── privacy.jsx ├── faq.jsx └── dashboard │ ├── blocks.jsx │ ├── my-clients.jsx │ ├── my-coaches.jsx │ ├── all-users.jsx │ └── bodyweight.jsx ├── LICENSE ├── README.md └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["tailwindcss"], 3 | }; 4 | -------------------------------------------------------------------------------- /utils/picture-utils.js: -------------------------------------------------------------------------------- 1 | export const pictureTypes = ["front", "side", "back"]; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tailwindConfig: './tailwind.config.js', 3 | }; 4 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/images/icon-192x192.png -------------------------------------------------------------------------------- /public/images/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/images/icon-256x256.png -------------------------------------------------------------------------------- /public/images/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/images/icon-384x384.png -------------------------------------------------------------------------------- /public/images/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/images/icon-512x512.png -------------------------------------------------------------------------------- /public/images/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/images/icon-96x96.png -------------------------------------------------------------------------------- /public/features/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/features/screenshot_1.png -------------------------------------------------------------------------------- /public/features/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/features/screenshot_2.png -------------------------------------------------------------------------------- /public/features/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/features/screenshot_3.png -------------------------------------------------------------------------------- /public/features/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/features/screenshot_4.png -------------------------------------------------------------------------------- /public/features/screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/features/screenshot_5.png -------------------------------------------------------------------------------- /public/features/screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/features/screenshot_6.png -------------------------------------------------------------------------------- /public/features/screenshot_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/features/screenshot_7.png -------------------------------------------------------------------------------- /public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakaton/repsetter/HEAD/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /public/fallback-GtbOZAaAkPqSfUfS3cKdI.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";self.fallback=async e=>"document"===e.destination?caches.match("/_offline",{ignoreSearch:!0}):Response.error()})(); -------------------------------------------------------------------------------- /utils/lazyload-config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | elements_selector: ".lazy", 3 | threshold: 100, 4 | callback_enter: (element) => { 5 | console.log("enter", element); 6 | }, 7 | callback_exit: (element) => { 8 | console.log("Exit", element); 9 | }, 10 | }; 11 | export default config; 12 | -------------------------------------------------------------------------------- /utils/get-stripe.js: -------------------------------------------------------------------------------- 1 | import { loadStripe } from '@stripe/stripe-js'; 2 | 3 | let stripePromise; 4 | const getStripe = () => { 5 | if (!stripePromise) { 6 | stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY); 7 | } 8 | return stripePromise; 9 | }; 10 | 11 | export default getStripe; 12 | -------------------------------------------------------------------------------- /utils/enforce-api-route-secret.js: -------------------------------------------------------------------------------- 1 | export default function enforceApiRouteSecret(req, res) { 2 | if ( 3 | (req.query.API_ROUTE_SECRET || req.body.API_ROUTE_SECRET) !== 4 | process.env.API_ROUTE_SECRET 5 | ) { 6 | res.status(401).send('You are not authorized to make this call'); 7 | return false; 8 | } 9 | 10 | return true; 11 | } 12 | -------------------------------------------------------------------------------- /components/MyLink.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import Link from 'next/link'; 3 | 4 | const MyLink = forwardRef((props, ref) => { 5 | const { href, as, children, ...rest } = props; 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | 12 | ); 13 | }); 14 | MyLink.displayName = 'MyLink'; 15 | 16 | export default MyLink; 17 | -------------------------------------------------------------------------------- /utils/weight-utils.js: -------------------------------------------------------------------------------- 1 | export const weightEvents = [ 2 | { name: "none", color: "rgb(255, 191, 102)" }, 3 | { name: "changed clothes", color: "rgb(240, 15, 247)" }, 4 | { name: "ate", color: "rgb(83, 242, 78)" }, 5 | { name: "drank", color: "rgb(73, 242, 234)" }, 6 | { name: "urinated", color: "rgb(252, 255, 59)" }, 7 | { name: "pooped", color: "rgb(117, 86, 0)" }, 8 | { name: "worked out", color: "rgb(255, 81, 69)" }, 9 | ]; 10 | 11 | export let weightEventColors = {}; 12 | weightEvents.forEach(({ name, color }) => (weightEventColors[name] = color)); 13 | -------------------------------------------------------------------------------- /styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | .style-links a { 7 | @apply text-blue-600 no-underline; 8 | } 9 | .style-links a:hover { 10 | @apply text-blue-500; 11 | } 12 | 13 | /* Chrome, Safari, Edge, Opera */ 14 | input.hide-arrows::-webkit-outer-spin-button, 15 | input.hide-arrows::-webkit-inner-spin-button { 16 | -webkit-appearance: none; 17 | margin: 0; 18 | } 19 | 20 | /* Firefox */ 21 | input.hide-arrows[type=number] { 22 | -moz-appearance: textfield; 23 | } 24 | } -------------------------------------------------------------------------------- /.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.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # TODO 37 | TODO 38 | 39 | # MISC 40 | *.txt 41 | repsetter.code-workspace 42 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | 3 | module.exports = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx}", 6 | "./components/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | screens: { 10 | xxs: "260px", 11 | xs: "360px", 12 | ...defaultTheme.screens, 13 | }, 14 | extend: { 15 | fontFamily: { 16 | sans: ["Inter var", ...defaultTheme.fontFamily.sans], 17 | }, 18 | }, 19 | }, 20 | plugins: [ 21 | require("@tailwindcss/typography"), 22 | require("@tailwindcss/forms"), 23 | require("@tailwindcss/aspect-ratio"), 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /components/dashboard/notification/DeleteAccountNotification.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useUser } from '../../../context/user-context'; 3 | import Notification from '../../Notification'; 4 | 5 | export default function DeleteAccountNotification() { 6 | const { didDeleteAccount } = useUser(); 7 | const [open, setOpen] = useState(false); 8 | const [status, setStatus] = useState(); 9 | 10 | useEffect(() => { 11 | if (didDeleteAccount) { 12 | setOpen(true); 13 | setStatus({ type: 'succeeded', title: 'Successfully deleted account' }); 14 | } 15 | }, [didDeleteAccount]); 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /public/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | Repsetter Logo 3 | 7 | 12 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withPWA = require("next-pwa"); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = withPWA({ 5 | reactStrictMode: false, 6 | images: { 7 | domains: [process.env.NEXT_PUBLIC_SUPABASE_URL.split("//")[1]], 8 | }, 9 | pwa: { 10 | dest: "public", 11 | register: true, 12 | skipWaiting: true, 13 | disable: process.env.NODE_ENV === "development", 14 | }, 15 | i18n: { 16 | locales: ["en"], 17 | defaultLocale: "en", 18 | }, 19 | compiler: 20 | process.env.NODE_ENV === "production" 21 | ? { 22 | removeConsole: { 23 | exclude: ["error"], 24 | }, 25 | } 26 | : {}, 27 | }); 28 | 29 | module.exports = nextConfig; 30 | -------------------------------------------------------------------------------- /utils/wii-balance-board/helpers.js: -------------------------------------------------------------------------------- 1 | export function toBigEndian(n, size) { 2 | var buffer = new Array(); 3 | 4 | n.toString(16) 5 | .match(/.{1,2}/g) 6 | ?.map((x) => { 7 | var v = "0x" + x; 8 | var a = Number(v); 9 | buffer.push(a); 10 | }); 11 | 12 | return buffer; 13 | } 14 | 15 | export function numbersToBuffer(data) { 16 | return new Int8Array(data); 17 | } 18 | 19 | export function debug(buffer, print = true) { 20 | let a = Array.prototype.map 21 | .call(new Uint8Array(buffer), (x) => ("00" + x.toString(16)).slice(-2)) 22 | .join("-"); 23 | if (print) console.log(a); 24 | return a; 25 | } 26 | 27 | export function getBitInByte(byte, index) { 28 | return byte & (1 << (index - 1)); 29 | } 30 | -------------------------------------------------------------------------------- /context/progress-context.jsx: -------------------------------------------------------------------------------- 1 | import { useState, createContext, useContext } from "react"; 2 | 3 | export const ProgressContext = createContext(); 4 | 5 | export function ProgressContextProvider(props) { 6 | const [progressFilters, setProgressFilters] = useState({ 7 | "date-range": "past month", 8 | }); 9 | const [progressContainsFilters, setProgressContainsFilters] = useState({}); 10 | 11 | const value = { 12 | progressFilters, 13 | setProgressFilters, 14 | progressContainsFilters, 15 | setProgressContainsFilters, 16 | }; 17 | return ; 18 | } 19 | 20 | export function useProgress() { 21 | const context = useContext(ProgressContext); 22 | return context; 23 | } 24 | -------------------------------------------------------------------------------- /components/layouts/Layout.jsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Header from "../Header"; 3 | import Footer from "../Footer"; 4 | 5 | import { useOnline } from "../../context/online-context"; 6 | import OfflineBanner from "../OfflineBanner"; 7 | import DeleteAccountNotification from "../dashboard/notification/DeleteAccountNotification"; 8 | 9 | export default function Layout({ children }) { 10 | const { online } = useOnline(); 11 | return ( 12 | <> 13 | 14 | Repsetter 15 | 16 |
17 | {!online && } 18 | 19 |
{children}
20 |