├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── lint.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── README.md ├── components ├── Activity │ ├── CurrentUser │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── App │ ├── Config.tsx │ └── index.scss ├── AppStoreDownloadButton │ └── index.tsx ├── AuthButton │ └── index.tsx ├── BlockUser │ ├── data.ts │ ├── index.tsx │ └── models.ts ├── Button │ └── index.tsx ├── CKEditor │ ├── Content │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Loader │ │ ├── index.module.scss │ │ └── index.tsx │ └── index.tsx ├── CardCell │ ├── Base │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Owned │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── CardSide │ ├── index.module.scss │ └── index.tsx ├── ConfirmationForm │ ├── ButtonContent │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── Dashboard │ ├── AddCards │ │ ├── Row │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── Cram │ │ ├── CardContainer │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Footer │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Navbar │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── ProgressModal │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── RateButton │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── RecapModal │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── RecapModalData │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── SliderRow │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Sliders │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── useCramState.ts │ ├── CreateDeck │ │ ├── data.ts │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── DeckPage │ │ ├── Cards │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Comments │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Controls │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Footer │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Header │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Navigation │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Preview │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── SimilarDecks │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── data.ts │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── Decks │ │ ├── Header │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── SectionContent │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Sections │ │ │ ├── index.tsx │ │ │ └── models.ts │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── EditCard │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── EditDeck │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── Home │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Interests │ │ ├── data.ts │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── Market │ │ ├── DeckRow │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── data.ts │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── Navbar │ │ ├── ProfileDropdown │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Tab │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Review │ │ ├── CardContainer │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Footer │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Navbar │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── ProgressModal │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── RateButton │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── RecapModal │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── RecapModalData │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── useReviewState.ts │ ├── Settings │ │ ├── Account │ │ │ ├── Contact │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── Email │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── ForgotPassword │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── Name │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── Profile │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── SignOut │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Develop │ │ │ ├── Api │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── GitHub │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Navigation │ │ │ ├── Link │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Notifications │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Sidebar │ │ ├── index.module.scss │ │ └── index.tsx │ ├── SidebarRow │ │ ├── index.module.scss │ │ └── index.tsx │ ├── SidebarSection │ │ ├── index.module.scss │ │ └── index.tsx │ ├── UserPage │ │ ├── Activity │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Bio │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Contact │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Decks │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── EditBio │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Image │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Level │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── data.ts │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── models.ts │ ├── index.module.scss │ └── index.tsx ├── DeckCell │ ├── Base │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Owned │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── Disqus │ ├── CommentCount │ │ └── index.tsx │ ├── DiscussionEmbed │ │ └── index.tsx │ └── index.tsx ├── Dropdown │ ├── index.module.scss │ └── index.tsx ├── Head │ └── index.tsx ├── Home │ ├── AuthButton │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Classroom │ │ ├── List │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Footer │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Header │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Preview │ │ ├── ClaimXPButton │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Footer │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── ProgressModal │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── RateButton │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Screenshots │ │ ├── index.module.scss │ │ └── index.tsx │ ├── SectionDivider │ │ ├── index.module.scss │ │ └── index.tsx │ ├── SpacedRepetition │ │ ├── index.module.scss │ │ └── index.tsx │ ├── WhiteArrowAuthButton │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── ImagePicker │ ├── index.module.scss │ └── index.tsx ├── Input │ ├── index.module.scss │ └── index.tsx ├── Landing │ ├── data.ts │ └── index.tsx ├── Loader │ ├── index.module.scss │ └── index.tsx ├── MarketSearchLink │ ├── index.module.scss │ └── index.tsx ├── Modal │ ├── ApiKey │ │ └── index.tsx │ ├── Auth │ │ ├── Providers │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Confirmation │ │ ├── index.module.scss │ │ └── index.tsx │ ├── ContactUser │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Copy │ │ ├── index.module.scss │ │ └── index.tsx │ ├── CreateSection │ │ └── index.tsx │ ├── Input │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Intro │ │ ├── index.module.scss │ │ └── index.tsx │ ├── RemoveDeck │ │ └── index.tsx │ ├── RenameSection │ │ └── index.tsx │ ├── ShareDeck │ │ └── index.tsx │ ├── ShareSection │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── Navbar │ ├── index.module.scss │ └── index.tsx ├── NotFound │ ├── index.module.scss │ └── index.tsx ├── Notification │ ├── index.module.scss │ └── index.tsx ├── Notifications │ ├── Option │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── Policy │ ├── index.module.scss │ └── index.tsx ├── Privacy │ └── index.tsx ├── Progress │ ├── constants.ts │ └── index.scss ├── PublishDeckContent │ ├── index.module.scss │ └── index.tsx ├── ReportMessage │ ├── data.ts │ ├── index.module.scss │ ├── index.tsx │ └── models.ts ├── RestrictContact │ ├── data.ts │ ├── index.tsx │ └── models.ts ├── Root │ └── index.tsx ├── Screenshot │ └── index.tsx ├── SectionHeader │ ├── Owned │ │ ├── index.module.scss │ │ └── index.tsx │ ├── ToggleExpandedButton │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── SortDropdown │ ├── index.module.scss │ └── index.tsx ├── Stars │ ├── Star │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── Support │ └── index.tsx ├── TextArea │ ├── index.module.scss │ └── index.tsx ├── TimePicker │ ├── index.module.scss │ └── index.tsx ├── TopGradient │ ├── index.module.scss │ └── index.tsx └── TripleDots │ ├── index.module.scss │ └── index.tsx ├── hooks ├── hideChat.ts ├── requiresAuth.ts ├── useAddCardsState.ts ├── useAllCards.ts ├── useAnalytics.ts ├── useAuthModal.ts ├── useAuthState.ts ├── useCard.ts ├── useCards.ts ├── useChat.ts ├── useCloseMessage.ts ├── useContactUserState.ts ├── useCreatedDeck.ts ├── useCreator.ts ├── useCurrentUser.ts ├── useDeck.ts ├── useDecks.ts ├── useExpandedSections.ts ├── useIsomorphicLayoutEffect.ts ├── useKeyPress.ts ├── useLayoutAuthState.ts ├── useLocalStorageBoolean.ts ├── useNotifications.tsx ├── useOnSignUp.ts ├── usePreview.ts ├── usePreviewDeck.ts ├── useProgress.ts ├── useRecommendedDecks.ts ├── useRemoveDeckModal.ts ├── useScreenshot.tsx ├── useSections.ts ├── useSelectedDeck.ts ├── useSimilarDecks.ts ├── useTopics.ts ├── useUrlForMarket.ts ├── useUser.ts └── useUserImageUrl.ts ├── images ├── app-store-download.svg ├── check.jpg ├── defaults │ ├── deck.jpg │ ├── user.jpg │ └── user.svg ├── favicon.png ├── home │ ├── bullet.svg │ ├── classroom.png │ ├── screenshot-background.svg │ └── spaced-repetition.png ├── icons │ ├── apple.svg │ ├── cart.svg │ ├── decks.svg │ ├── download.svg │ ├── edit.svg │ ├── google.svg │ ├── gray-left-arrow-head.svg │ ├── gray-right-arrow-head.svg │ ├── gray-star.svg │ ├── home.svg │ ├── left-arrow-head.svg │ ├── left-arrow.svg │ ├── pencil.svg │ ├── share.svg │ ├── sort.svg │ ├── star.jpg │ ├── times.svg │ ├── toggle.svg │ ├── topics.svg │ └── users.svg ├── logos │ ├── capital-inverted-grayscale.jpg │ ├── capital-inverted.jpg │ ├── capital.jpg │ ├── large.png │ └── transparent.jpg ├── screenshots │ ├── cram.jpg │ ├── editor.jpg │ ├── home.jpg │ ├── market.jpg │ ├── recap.jpg │ ├── review.jpg │ └── sections.jpg └── topics │ ├── Anatomy.jpg │ ├── Biology.jpg │ ├── Calculus.jpg │ ├── Competition Prep.jpg │ ├── Geography.jpg │ ├── History.jpg │ ├── Law.jpg │ ├── Literature.jpg │ ├── Music.jpg │ ├── Pathology.jpg │ ├── Physics.jpg │ ├── SAT Prep.jpg │ ├── code.jpg │ ├── iOS Dev.jpg │ ├── language.jpg │ ├── math.jpg │ ├── politics.jpg │ └── science.jpg ├── lib ├── cache │ ├── cards.ts │ ├── deckList.ts │ ├── messages.ts │ ├── userList.ts │ └── users.ts ├── constants.ts ├── expectsSignIn.ts ├── firebase │ ├── admin.ts │ └── index.ts ├── flattenQuery.ts ├── formatDate.ts ├── formatNumber.ts ├── getActivity.ts ├── getCards.ts ├── getCreatedDecks.ts ├── getDeck.ts ├── getDeckBySlugId.ts ├── getDecks.ts ├── getNumberOfDecks.ts ├── getPreviewDeck.ts ├── getSections.ts ├── getStorageUrl.ts ├── getToken.ts ├── getTopics.ts ├── getUserFromSlugId.ts ├── getUsers.ts ├── handleError.ts ├── identify │ ├── analytics.ts │ ├── hubspot.ts │ └── index.ts ├── includesNormalized.ts ├── ios.ts ├── randomEmoji.ts ├── rankingToString.ts ├── resetUserImage.ts ├── setToken.ts ├── sleep.ts ├── slugify.ts ├── toOneDecimalPlace.ts └── uploadUserImage.ts ├── models ├── ActivityNode.ts ├── AuthenticationMode.ts ├── Cache.ts ├── Card │ ├── UserData.ts │ └── index.ts ├── Deck │ ├── Search.ts │ ├── UserData.ts │ └── index.ts ├── LoadingState.ts ├── Message.ts ├── PerformanceRating.ts ├── PreviewDeck.ts ├── Router.ts ├── Section.ts ├── SnapshotLike.ts ├── Topic │ ├── Category.ts │ └── index.ts └── User │ ├── CreateOptions.ts │ ├── Data.ts │ ├── Notifications.ts │ └── index.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── _document.tsx ├── api │ ├── sitemap.ts │ ├── uploadDeckAsset.ts │ └── uploadUserAsset.ts ├── block │ └── [fromId] │ │ └── [toId].tsx ├── cram │ └── [slugId] │ │ └── [slug] │ │ ├── [sectionId].tsx │ │ └── index.tsx ├── d │ └── [slugId] │ │ └── [slug].tsx ├── decks │ ├── [slugId] │ │ └── [slug] │ │ │ ├── add │ │ │ ├── [sectionId].tsx │ │ │ └── index.tsx │ │ │ ├── edit │ │ │ └── [cardId].tsx │ │ │ ├── index.tsx │ │ │ └── u │ │ │ └── [unlockSectionId].tsx │ └── index.tsx ├── edit │ └── [slugId] │ │ └── [slug].tsx ├── index.tsx ├── interests.tsx ├── landing.tsx ├── market.tsx ├── new.tsx ├── privacy.tsx ├── report │ └── [fromId] │ │ └── [toId] │ │ └── message │ │ └── [messageId].tsx ├── restrict-contact │ └── [id].tsx ├── review │ ├── [slugId] │ │ └── [slug] │ │ │ ├── [sectionId].tsx │ │ │ └── index.tsx │ └── index.tsx ├── settings │ ├── develop.tsx │ ├── index.tsx │ └── notifications.tsx ├── support.tsx └── u │ └── [slugId] │ └── [slug].tsx ├── public ├── ads.txt ├── favicon.png ├── firebase-messaging-sw.js ├── images │ └── logos │ │ ├── capital-inverted-grayscale.png │ │ ├── capital-inverted.png │ │ ├── email-header.png │ │ ├── large.jpg │ │ ├── large.png │ │ ├── square.jpg │ │ └── square.png ├── manifest.webmanifest └── robots.txt ├── state ├── activity.ts ├── addCards.ts ├── authModal.ts ├── cards.ts ├── contactUser.ts ├── createDeck.ts ├── createdDecks.ts ├── creators.ts ├── currentUser.ts ├── decks.ts ├── expandedSections.ts ├── introModalIsShowing.ts ├── newUserImageUrl.ts ├── previewDeck.ts ├── search.ts ├── sections.ts ├── similarDecks.ts ├── topics.ts └── users.ts ├── styles ├── _card-cell.scss ├── _card-side.scss ├── _chat.scss ├── _ck-editor.scss ├── _colors.scss ├── _dashboard.scss ├── _font.scss ├── _geometry.scss ├── _gradient.scss ├── _raw.scss ├── _shadow.scss ├── _text.scss └── _z-index.scss └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-logical-assignment-operators", 5 | "react-optimized-image/plugin" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vercel/ 3 | .next/ 4 | out/ 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "prettier"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier", 10 | "prettier/prettier" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/explicit-module-boundary-types": 0, 14 | "no-throw-literal": 2, 15 | "eqeqeq": [2, "always"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - kenmueller 3 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - staging 6 | 7 | jobs: 8 | test: 9 | name: Lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@main 14 | - name: Set up Node 15 | uses: actions/setup-node@main 16 | with: 17 | node-version: '12' 18 | check-latest: true 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Lint 22 | run: npm run lint 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.DS_Store 2 | node_modules/ 3 | .vercel/ 4 | .next/ 5 | out/ 6 | .env 7 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname $0)/_/husky.sh" 3 | 4 | npm run lint && npm run format 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vercel/ 3 | .next/ 4 | out/ 5 | *.hbs 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [memorize.ai](https://memorize.ai) 2 | 3 | Also see [the iOS repository](https://github.com/memorize-ai/ios) 4 | 5 | ## Get started 6 | 7 | ### Clone 8 | 9 | > Development is done on the `staging` branch 10 | 11 | ```bash 12 | git clone -b staging https://github.com/memorize-ai/web.git 13 | ``` 14 | 15 | ### Install dependencies 16 | 17 | ```bash 18 | npm i 19 | ``` 20 | 21 | ### Start local server 22 | 23 | ```bash 24 | npm run dev 25 | ``` 26 | 27 | ## Notes 28 | 29 | - Performance rating encoding: 30 | - `Easy: 0` 31 | - `Struggled: 1` 32 | - `Forgot: 2` 33 | -------------------------------------------------------------------------------- /components/Activity/CurrentUser/index.tsx: -------------------------------------------------------------------------------- 1 | import useCurrentUser from 'hooks/useCurrentUser' 2 | import Activity from '..' 3 | 4 | export interface CurrentUserActivityProps { 5 | className?: string 6 | } 7 | 8 | const CurrentUserActivity = ({ className }: CurrentUserActivityProps) => { 9 | const [currentUser] = useCurrentUser() 10 | return 11 | } 12 | 13 | export default CurrentUserActivity 14 | -------------------------------------------------------------------------------- /components/App/Config.tsx: -------------------------------------------------------------------------------- 1 | import useAnalytics from 'hooks/useAnalytics' 2 | import useNotifications from 'hooks/useNotifications' 3 | 4 | const AppConfig = () => { 5 | useAnalytics() 6 | useNotifications() 7 | 8 | return null 9 | } 10 | 11 | export default AppConfig 12 | -------------------------------------------------------------------------------- /components/AppStoreDownloadButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react' 2 | import { Svg } from 'react-optimized-image' 3 | 4 | import { APP_STORE_URL } from 'lib/constants' 5 | import icon from 'images/app-store-download.svg' 6 | 7 | export type AppStoreDownloadButtonProps = HTMLAttributes 8 | 9 | const AppStoreDownloadButton = (props: AppStoreDownloadButtonProps) => ( 10 | 16 | 17 | 18 | ) 19 | 20 | export default AppStoreDownloadButton 21 | -------------------------------------------------------------------------------- /components/AuthButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, useCallback, MouseEvent } from 'react' 2 | 3 | import useAuthModal from 'hooks/useAuthModal' 4 | import AuthenticationMode from 'models/AuthenticationMode' 5 | import { APP_STORE_URL } from 'lib/constants' 6 | import { isIosHandheld } from 'lib/ios' 7 | 8 | export interface AuthButtonProps 9 | extends ButtonHTMLAttributes { 10 | signUp?: boolean 11 | goToAppStoreIfHandheldIos?: boolean 12 | } 13 | 14 | const AuthButton = ({ 15 | signUp = false, 16 | goToAppStoreIfHandheldIos = false, 17 | ...props 18 | }: AuthButtonProps) => { 19 | const { setIsShowing, setMode } = useAuthModal() 20 | 21 | const onClick = useCallback( 22 | (event: MouseEvent) => { 23 | event.stopPropagation() 24 | 25 | if (goToAppStoreIfHandheldIos && isIosHandheld()) { 26 | window.location.href = APP_STORE_URL 27 | return 28 | } 29 | 30 | if (signUp) setMode(AuthenticationMode.SignUp) 31 | 32 | setIsShowing(true) 33 | }, 34 | [signUp, goToAppStoreIfHandheldIos, setMode, setIsShowing] 35 | ) 36 | 37 | return 50 | ) 51 | 52 | export default Button 53 | -------------------------------------------------------------------------------- /components/CKEditor/Content/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/font'; 2 | @use 'styles/ck-editor'; 3 | 4 | $toolbar-height: 40.2px; 5 | $min-editable-height: 200px; 6 | 7 | .root :global { 8 | @include ck-editor.dropdown { 9 | flex-wrap: wrap; 10 | } 11 | 12 | .ck.ck-editor { 13 | height: 100% !important; 14 | 15 | * { 16 | font-family: font.$family; 17 | } 18 | 19 | &__main { 20 | height: calc(100% - #{$toolbar-height}); 21 | } 22 | 23 | &__editable { 24 | min-height: $min-editable-height; 25 | height: 100%; 26 | } 27 | } 28 | 29 | .ck.ck-content * { 30 | font-weight: initial; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/CKEditor/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback } from 'react' 2 | import { CKEditor as Base } from '@ckeditor/ckeditor5-react' 3 | import Editor from 'ckeditor5-memorize.ai' 4 | import cx from 'classnames' 5 | 6 | import styles from './index.module.scss' 7 | 8 | export interface CKEditorProps { 9 | className?: string 10 | uploadUrl: string 11 | data: string 12 | setData(data: string): void 13 | } 14 | 15 | const CKEditor = ({ className, uploadUrl, data, setData }: CKEditorProps) => { 16 | const config = useMemo( 17 | () => ({ 18 | simpleUpload: { uploadUrl } 19 | }), 20 | [uploadUrl] 21 | ) 22 | 23 | const onChange = useCallback( 24 | (_event, editor) => { 25 | setData(editor.getData()) 26 | }, 27 | [setData] 28 | ) 29 | 30 | return ( 31 |
32 | 33 |
34 | ) 35 | } 36 | 37 | export default CKEditor 38 | -------------------------------------------------------------------------------- /components/CKEditor/Loader/index.module.scss: -------------------------------------------------------------------------------- 1 | $toolbar-height: 40.2px; 2 | $min-editable-height: 200px; 3 | 4 | .root { 5 | display: grid; 6 | justify-content: center; 7 | align-content: center; 8 | height: $toolbar-height + $min-editable-height; 9 | border: 1px solid #c4c4c4; 10 | border-radius: 2px; 11 | } 12 | -------------------------------------------------------------------------------- /components/CKEditor/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import Loader from 'components/Loader' 2 | 3 | import styles from './index.module.scss' 4 | 5 | const CKEditorLoader = () => ( 6 |
7 | 8 |
9 | ) 10 | 11 | export default CKEditorLoader 12 | -------------------------------------------------------------------------------- /components/CKEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | 3 | import Loader from './Loader' 4 | 5 | export default dynamic(() => import('./Content'), { 6 | ssr: false, 7 | loading: () => 8 | }) 9 | -------------------------------------------------------------------------------- /components/CardCell/Base/index.tsx: -------------------------------------------------------------------------------- 1 | import Card from 'models/Card' 2 | import CardSide from 'components/CardSide' 3 | 4 | import styles from './index.module.scss' 5 | 6 | export interface CardCellBaseProps { 7 | card: Card 8 | } 9 | 10 | const CardCellBase = ({ card }: CardCellBaseProps) => ( 11 |
12 |
13 | 14 | {card.front} 15 | 16 | Front 17 |
18 |
19 |
20 | {card.back} 21 | Back 22 |
23 |
24 | ) 25 | 26 | export default CardCellBase 27 | -------------------------------------------------------------------------------- /components/CardCell/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/card-cell'; 2 | 3 | .root { 4 | @include card-cell.root; 5 | } 6 | -------------------------------------------------------------------------------- /components/CardCell/index.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames' 2 | 3 | import Card from 'models/Card' 4 | import Base from './Base' 5 | 6 | import styles from './index.module.scss' 7 | 8 | export interface CardCellProps { 9 | className?: string 10 | card: Card 11 | } 12 | 13 | const CardCell = ({ className, card }: CardCellProps) => ( 14 |
20 | 21 |
22 | ) 23 | 24 | export default CardCell 25 | -------------------------------------------------------------------------------- /components/ConfirmationForm/ButtonContent/index.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 2 | import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons' 3 | 4 | import LoadingState from 'models/LoadingState' 5 | import Loader from 'components/Loader' 6 | 7 | export interface ConfirmationFormButtonContentProps { 8 | loadingState: LoadingState 9 | text: string 10 | } 11 | 12 | const ConfirmationFormButtonContent = ({ 13 | loadingState, 14 | text 15 | }: ConfirmationFormButtonContentProps) => { 16 | switch (loadingState) { 17 | case LoadingState.None: 18 | return <>{text} 19 | case LoadingState.Loading: 20 | return 21 | case LoadingState.Success: 22 | return 23 | case LoadingState.Fail: 24 | return 25 | } 26 | } 27 | 28 | export default ConfirmationFormButtonContent 29 | -------------------------------------------------------------------------------- /components/Dashboard/AddCards/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | export interface AddCardsQuery extends ParsedUrlQuery { 4 | slugId: string 5 | slug: string 6 | sectionId: string 7 | } 8 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/Footer/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | $vertical-offset: 100px; 4 | 5 | .root { 6 | display: grid; 7 | justify-items: center; 8 | align-items: center; 9 | padding: 40px 0; 10 | } 11 | 12 | .waitingForRating { 13 | .message { 14 | opacity: 0; 15 | transform: translateY($vertical-offset); 16 | } 17 | 18 | .buttons { 19 | opacity: 1; 20 | transform: none; 21 | } 22 | } 23 | 24 | .message, 25 | .buttons { 26 | grid-row: 1; 27 | grid-column: 1; 28 | transition: opacity 0.3s, transform 0.3s; 29 | } 30 | 31 | .message { 32 | text-align: center; 33 | font-size: 20px; 34 | font-weight: 900; 35 | color: colors.$dark-gray; 36 | opacity: 0.8; 37 | } 38 | 39 | .buttons { 40 | display: flex; 41 | opacity: 0; 42 | transform: translateY($vertical-offset); 43 | } 44 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/Navbar/index.module.scss: -------------------------------------------------------------------------------- 1 | $button-height: 40px; 2 | 3 | .root { 4 | display: flex; 5 | align-items: center; 6 | overflow-x: auto; 7 | margin-top: 20px; 8 | } 9 | 10 | .back, 11 | .progress, 12 | .skip, 13 | .recap { 14 | flex-shrink: 0; 15 | } 16 | 17 | .back, 18 | .skip, 19 | .recap { 20 | height: $button-height; 21 | color: white; 22 | border: 2px solid transparentize(#eee, 0.8); 23 | border-radius: 8px; 24 | transition: opacity 0.3s, border-color 0.3s; 25 | 26 | &:hover { 27 | opacity: 0.5; 28 | border-color: transparentize(#eee, 0.6); 29 | } 30 | } 31 | 32 | .back { 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | width: $button-height; 37 | margin-right: 12px; 38 | 39 | &:hover .backIcon { 40 | transform: scale(1.7); 41 | } 42 | } 43 | 44 | .backIcon { 45 | transform: scale(1.5); 46 | transition: transform 0.3s; 47 | } 48 | 49 | .progress, 50 | .skip, 51 | .recap { 52 | font-weight: 900; 53 | } 54 | 55 | .progress { 56 | margin-right: auto; 57 | font-size: 25px; 58 | color: white; 59 | transform: translateY(-1px); 60 | } 61 | 62 | .skip, 63 | .recap { 64 | padding: 0 12px; 65 | text-transform: uppercase; 66 | font-size: 18px; 67 | } 68 | 69 | .skip { 70 | margin: 0 12px; 71 | } 72 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/ProgressModal/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | $margin: 20px; 4 | 5 | .root { 6 | display: flex; 7 | position: relative; 8 | flex-direction: column; 9 | align-items: center; 10 | max-width: 650px; 11 | width: calc(100vw - #{$margin * 2}); 12 | max-height: calc(100vh - #{$margin * 2}); 13 | overflow-y: auto; 14 | margin: $margin; 15 | padding: 50px; 16 | background: #051e34; 17 | border-radius: 15px; 18 | } 19 | 20 | .badges { 21 | $inset: 20px; 22 | 23 | display: flex; 24 | position: absolute; 25 | top: $inset; 26 | left: $inset; 27 | } 28 | 29 | .xp, 30 | .streak { 31 | padding: 2px 8px; 32 | font-size: 18px; 33 | font-weight: 900; 34 | border-radius: 5px; 35 | 36 | &:not(:last-child) { 37 | margin-right: 8px; 38 | } 39 | } 40 | 41 | .xp { 42 | color: colors.$dark-gray; 43 | background: #ffcb6b; 44 | } 45 | 46 | .streak { 47 | color: white; 48 | background: #06ba7a; 49 | } 50 | 51 | .emoji { 52 | margin-bottom: 20px; 53 | font-size: 80px; 54 | } 55 | 56 | .message { 57 | text-align: center; 58 | font-weight: 900; 59 | font-size: 40px; 60 | color: white; 61 | } 62 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/ProgressModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { CramProgressData, CRAM_MASTERED_STREAK } from '../useCramState' 2 | import Modal from 'components/Modal' 3 | 4 | import styles from './index.module.scss' 5 | 6 | export interface CramProgressModalProps { 7 | data: CramProgressData | null 8 | isShowing: boolean 9 | setIsShowing(isShowing: boolean): void 10 | } 11 | 12 | const CramProgressModal = ({ 13 | data, 14 | isShowing, 15 | setIsShowing 16 | }: CramProgressModalProps) => { 17 | const isMastered = (data?.streak ?? 0) >= CRAM_MASTERED_STREAK 18 | const didEarnXp = (data?.xp ?? 0) > 0 19 | 20 | return ( 21 | 26 |
27 | {data && didEarnXp &&

+{data.xp} xp

} 28 |

29 | {data?.streak} / {CRAM_MASTERED_STREAK} streak 30 |

31 |
32 | 33 | {isMastered ? '🥳' : data?.emoji} 34 | 35 |

36 | {isMastered ? 'Mastered!' : data?.message} 37 |

38 |
39 | ) 40 | } 41 | 42 | export default CramProgressModal 43 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/RateButton/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | height: 52px; 8 | padding: 0 14px; 9 | background: white; 10 | border: 1px solid #eee; 11 | border-radius: 8px; 12 | transition: border-color 0.3s, box-shadow 0.3s; 13 | 14 | @media (min-width: 400px) { 15 | width: 100px; 16 | padding: 0; 17 | } 18 | 19 | @media (min-width: 500px) { 20 | width: 130px; 21 | } 22 | 23 | @media (min-width: 600px) { 24 | width: 165px; 25 | } 26 | 27 | &:hover { 28 | border-color: white; 29 | box-shadow: 0 4px 8px transparentize(black, 0.9); 30 | } 31 | 32 | &:not(:last-child) { 33 | margin-right: 12px; 34 | } 35 | } 36 | 37 | .emoji { 38 | display: none; 39 | margin-right: 8px; 40 | font-size: 20px; 41 | 42 | @media (min-width: 500px) { 43 | display: block; 44 | } 45 | 46 | @media (min-width: 600px) { 47 | margin-right: 12px; 48 | font-size: 24px; 49 | } 50 | } 51 | 52 | .title { 53 | text-transform: uppercase; 54 | font-size: 9px; 55 | font-weight: 900; 56 | color: colors.$dark-gray; 57 | 58 | @media (min-width: 340px) { 59 | font-size: 12px; 60 | } 61 | 62 | @media (min-width: 600px) { 63 | font-size: 16px; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/RateButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, MouseEvent } from 'react' 2 | 3 | import PerformanceRating from 'models/PerformanceRating' 4 | 5 | import styles from './index.module.scss' 6 | 7 | export interface CramRateButtonProps { 8 | emoji: string 9 | title: string 10 | subtitle: string 11 | rate(rating: PerformanceRating): void 12 | rating: PerformanceRating 13 | } 14 | 15 | const CramRateButton = ({ 16 | emoji, 17 | title, 18 | subtitle, 19 | rate, 20 | rating 21 | }: CramRateButtonProps) => { 22 | const onClick = useCallback( 23 | (event: MouseEvent) => { 24 | event.stopPropagation() 25 | rate(rating) 26 | }, 27 | [rate, rating] 28 | ) 29 | 30 | return ( 31 | 40 | ) 41 | } 42 | 43 | export default CramRateButton 44 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/RecapModal/index.module.scss: -------------------------------------------------------------------------------- 1 | $margin: 20px; 2 | 3 | .root { 4 | display: flex; 5 | flex-direction: column; 6 | max-width: 650px; 7 | width: calc(100vw - #{$margin * 2}); 8 | max-height: calc(100vh - #{$margin * 2}); 9 | overflow-y: auto; 10 | margin: $margin; 11 | padding: 20px; 12 | background: #051e34; 13 | border-radius: 15px; 14 | } 15 | 16 | .emoji { 17 | margin-bottom: -15px; 18 | text-align: center; 19 | font-size: 80px; 20 | } 21 | 22 | .data { 23 | color: white; 24 | } 25 | 26 | .section { 27 | color: #c792ea; 28 | } 29 | 30 | .count { 31 | color: #89ddff; 32 | } 33 | 34 | .done { 35 | margin: 20px 0 0 auto; 36 | padding: 6px 20px; 37 | font-size: 20px; 38 | font-weight: 900; 39 | color: white; 40 | background: #0288d1; 41 | border-radius: 13px; 42 | transition: color 0.3s, background 0.3s; 43 | 44 | &:hover { 45 | color: #0288d1; 46 | background: white; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/RecapModalData/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-top: 20px; 3 | } 4 | 5 | .title { 6 | text-transform: uppercase; 7 | font-weight: 900; 8 | color: white; 9 | } 10 | 11 | .content { 12 | font-weight: bold; 13 | color: transparentize(white, 0.5); 14 | } 15 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/RecapModalData/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | import styles from './index.module.scss' 4 | 5 | export interface CramRecapModalDataProps { 6 | title: string 7 | children?: ReactNode 8 | } 9 | 10 | const CramRecapModalData = ({ title, children }: CramRecapModalDataProps) => ( 11 |
12 |

{title}

13 |

{children}

14 |
15 | ) 16 | 17 | export default CramRecapModalData 18 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/SliderRow/index.module.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | text-align: right; 3 | font-weight: 900; 4 | color: white; 5 | transition: opacity 0.3s; 6 | } 7 | 8 | .disabled { 9 | opacity: 0.7; 10 | } 11 | 12 | .slider { 13 | width: 100%; 14 | transform: translateY(1px); 15 | } 16 | 17 | .sliderContent, 18 | .sliderInnerContent { 19 | $height: 4px; 20 | 21 | height: $height; 22 | border-radius: $height / 2; 23 | } 24 | 25 | .sliderContent { 26 | background: transparentize(white, 0.5); 27 | } 28 | 29 | .sliderInnerContent { 30 | background: white; 31 | transition: width 0.3s; 32 | } 33 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/SliderRow/index.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames' 2 | 3 | import styles from './index.module.scss' 4 | 5 | export interface CramSliderRowProps { 6 | fill: number 7 | children: string 8 | } 9 | 10 | const CramSliderRow = ({ fill, children: title }: CramSliderRowProps) => ( 11 | 12 | {title} 13 | 14 |
15 |
19 |
20 | 21 | 22 | ) 23 | 24 | export default CramSliderRow 25 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/Sliders/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | align-self: center; 3 | max-width: 600px; 4 | width: 100%; 5 | margin-top: 20px; 6 | border-collapse: separate; 7 | border-spacing: 15px 10px; 8 | } 9 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/Sliders/index.tsx: -------------------------------------------------------------------------------- 1 | import Row from '../SliderRow' 2 | 3 | import styles from './index.module.scss' 4 | 5 | export interface CramSlidersProps { 6 | mastered: number 7 | seen: number 8 | unseen: number 9 | total: number 10 | } 11 | 12 | const CramSliders = ({ mastered, seen, unseen, total }: CramSlidersProps) => ( 13 | 14 | 15 | Mastered 16 | Seen 17 | Unseen 18 | 19 |
20 | ) 21 | 22 | export default CramSliders 23 | -------------------------------------------------------------------------------- /components/Dashboard/Cram/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/dashboard'; 2 | 3 | @include dashboard.sidebar-collapse(980px); 4 | 5 | .content { 6 | display: flex; 7 | flex-direction: column; 8 | height: 100vh !important; 9 | overflow: hidden; 10 | padding: 0 var(--horizontal-padding); 11 | } 12 | -------------------------------------------------------------------------------- /components/Dashboard/CreateDeck/data.ts: -------------------------------------------------------------------------------- 1 | import { GetStaticProps } from 'next' 2 | 3 | import { CreateDeckProps } from './models' 4 | import getTopics from 'lib/getTopics' 5 | 6 | const REVALIDATE = 3600 // 1 hour 7 | 8 | export const getStaticProps: GetStaticProps< 9 | CreateDeckProps, 10 | Record 11 | > = async () => ({ 12 | props: { topics: await getTopics() }, 13 | revalidate: REVALIDATE 14 | }) 15 | -------------------------------------------------------------------------------- /components/Dashboard/CreateDeck/models.ts: -------------------------------------------------------------------------------- 1 | import { TopicData } from 'models/Topic' 2 | 3 | export interface CreateDeckProps { 4 | topics: TopicData[] 5 | } 6 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/Cards/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | margin: 0 var(--inner-horizontal-padding); 5 | } 6 | 7 | .title { 8 | font-size: 24px; 9 | font-weight: bold; 10 | color: colors.$dark-gray; 11 | } 12 | 13 | .count { 14 | color: #582efe; 15 | } 16 | 17 | .sections { 18 | margin-top: 20px; 19 | } 20 | 21 | .section:not(:last-child) { 22 | margin-bottom: 12px; 23 | } 24 | 25 | .cards { 26 | margin-top: 12px; 27 | } 28 | 29 | .card:not(:last-child) { 30 | margin-bottom: 16px; 31 | } 32 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/Comments/index.module.scss: -------------------------------------------------------------------------------- 1 | $margin: 30px; 2 | 3 | .root { 4 | margin: $margin var(--inner-horizontal-padding) 0; 5 | } 6 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/Comments/index.tsx: -------------------------------------------------------------------------------- 1 | import Deck from 'models/Deck' 2 | import DiscussionEmbed from 'components/Disqus/DiscussionEmbed' 3 | 4 | import styles from './index.module.scss' 5 | 6 | export interface DeckPageCommentsProps { 7 | deck: Deck 8 | } 9 | 10 | const DeckPageComments = ({ deck }: DeckPageCommentsProps) => ( 11 |
12 | 13 |
14 | ) 15 | 16 | export default DeckPageComments 17 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/Footer/index.module.scss: -------------------------------------------------------------------------------- 1 | $margin: 30px; 2 | $topic-dimension: 90px; 3 | $topic-padding: 8px; 4 | 5 | .root { 6 | margin-top: 24px; 7 | } 8 | 9 | .description { 10 | max-width: 800px; 11 | margin: 0 $margin; 12 | font-size: 16px; 13 | color: #5c5c5c; 14 | } 15 | 16 | .topics { 17 | display: flex; 18 | overflow-x: auto; 19 | margin-top: 20px; 20 | padding: 0 $margin; 21 | } 22 | 23 | .topic { 24 | flex-shrink: 0; 25 | position: relative; 26 | overflow: hidden; 27 | width: $topic-dimension; 28 | height: $topic-dimension; 29 | background-size: cover; 30 | background-position: center; 31 | border-radius: 5px; 32 | 33 | &:not(:last-child) { 34 | margin-right: 12px; 35 | } 36 | } 37 | 38 | .topicName { 39 | position: absolute; 40 | right: $topic-padding / 2; 41 | bottom: $topic-padding; 42 | left: $topic-padding / 2; 43 | text-align: center; 44 | word-wrap: break-word; 45 | font-size: 12px; 46 | font-weight: bold; 47 | color: white; 48 | } 49 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import Deck from 'models/Deck' 4 | import Topic from 'models/Topic' 5 | 6 | import styles from './index.module.scss' 7 | 8 | export interface DeckPageFooterProps { 9 | deck: Deck 10 | topics: Topic[] 11 | } 12 | 13 | const DeckPageFooter = ({ deck, topics }: DeckPageFooterProps) => ( 14 |
15 |

{deck.description}

16 | {topics.length > 0 && ( 17 |
18 | {topics.map((topic, i) => ( 19 | 20 | 25 | 26 | 27 | 28 | 29 | {topic.name} 30 | 31 | 32 | 33 | ))} 34 |
35 | )} 36 |
37 | ) 38 | 39 | export default DeckPageFooter 40 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/Navigation/index.module.scss: -------------------------------------------------------------------------------- 1 | $horizontal-margin: 16px; 2 | $vertical-margin: 12px; 3 | 4 | .root { 5 | display: flex; 6 | align-items: center; 7 | margin: $vertical-margin var(--horizontal-padding); 8 | } 9 | 10 | .new { 11 | $dimension: 45px; 12 | 13 | display: flex; 14 | align-items: center; 15 | width: $dimension; 16 | height: $dimension; 17 | padding: 0 14px; 18 | color: white; 19 | border: 1.5px solid transparentize(#eee, 0.8); 20 | border-radius: 8px; 21 | 22 | &:hover, 23 | &:focus { 24 | .newIcon { 25 | opacity: 1; 26 | } 27 | } 28 | } 29 | 30 | .newIcon { 31 | opacity: 0.7; 32 | transform: scale(1.1); 33 | transition: opacity 0.3s; 34 | } 35 | 36 | .search { 37 | width: 100%; 38 | margin: 0 $horizontal-margin; 39 | } 40 | 41 | .searchInput { 42 | padding: 12px 12px 12px 44px; 43 | color: white; 44 | background: transparentize(#eee, 0.8); 45 | border: none; 46 | border-radius: 100px; 47 | 48 | &::placeholder { 49 | color: transparentize(#eee, 0.2); 50 | } 51 | } 52 | 53 | .searchIcon { 54 | top: 16px; 55 | left: 16px; 56 | color: #eee !important; 57 | } 58 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/SimilarDecks/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | margin: 32px 0 16px; 5 | } 6 | 7 | .title { 8 | margin: 0 var(--inner-horizontal-padding); 9 | font-size: 20px; 10 | font-weight: bold; 11 | color: colors.$dark-gray; 12 | 13 | @media (min-width: 425px) { 14 | font-size: 24px; 15 | } 16 | } 17 | 18 | .count { 19 | display: none; 20 | color: #582efe; 21 | 22 | @media (min-width: 365px) { 23 | display: inline; 24 | } 25 | } 26 | 27 | .rows { 28 | $margin-top: 12px; 29 | $padding-top: 4px; 30 | 31 | overflow-x: auto; 32 | margin-top: $margin-top - $padding-top; 33 | padding: $padding-top var(--inner-horizontal-padding) 16px; 34 | } 35 | 36 | .row { 37 | display: flex; 38 | 39 | &:not(:last-child) { 40 | margin-bottom: 12px; 41 | } 42 | } 43 | 44 | .deck { 45 | flex-shrink: 0; 46 | 47 | &:not(:last-child) { 48 | margin-right: 12px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/shadow'; 2 | @use 'styles/dashboard'; 3 | 4 | @include dashboard.sidebar-collapse(1150px); 5 | 6 | .content { 7 | display: grid; 8 | grid: auto 1fr / calc(100vw - var(--sidebar-width)); 9 | scroll-behavior: smooth; 10 | } 11 | 12 | .box { 13 | @include shadow.large; 14 | 15 | height: max-content; 16 | overflow: hidden; 17 | background: white; 18 | border-radius: 8px; 19 | margin: 0 var(--horizontal-padding) var(--horizontal-padding); 20 | padding-bottom: var(--inner-horizontal-padding); 21 | } 22 | -------------------------------------------------------------------------------- /components/Dashboard/DeckPage/models.ts: -------------------------------------------------------------------------------- 1 | import UserData from 'models/User/Data' 2 | import { DeckData } from 'models/Deck' 3 | import { SectionData } from 'models/Section' 4 | import { CardData } from 'models/Card' 5 | import { TopicData } from 'models/Topic' 6 | import { ParsedUrlQuery } from 'querystring' 7 | 8 | export interface DeckPageQuery extends ParsedUrlQuery { 9 | slugId: string 10 | slug: string 11 | } 12 | 13 | export interface DeckPageProps { 14 | decks: number 15 | deck: DeckData 16 | creator: UserData 17 | sections: SectionData[] 18 | cards: CardData[] 19 | topics: TopicData[] 20 | } 21 | -------------------------------------------------------------------------------- /components/Dashboard/Decks/SectionContent/index.module.scss: -------------------------------------------------------------------------------- 1 | .root:not(:last-child) { 2 | margin-bottom: 12px; 3 | 4 | .cards { 5 | margin-bottom: 20px; 6 | } 7 | } 8 | 9 | .addCards { 10 | display: flex; 11 | justify-content: center; 12 | margin-top: 12px; 13 | } 14 | 15 | .addCardsLink { 16 | display: flex; 17 | align-items: center; 18 | padding: 8px 14px; 19 | color: white; 20 | background: #582efe; 21 | border-radius: 8px; 22 | transition: background 0.3s; 23 | 24 | &:hover { 25 | background: #051e34; 26 | } 27 | } 28 | 29 | .addCardsIcon { 30 | margin-right: 10px; 31 | transform: scale(1.2); 32 | } 33 | 34 | .addCardsText { 35 | font-weight: 900; 36 | } 37 | 38 | .cards { 39 | margin-top: 20px; 40 | } 41 | 42 | .card:not(:last-child) { 43 | margin-bottom: 16px; 44 | } 45 | 46 | .loader { 47 | margin: 12px auto; 48 | } 49 | -------------------------------------------------------------------------------- /components/Dashboard/Decks/Sections/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | import Deck from 'models/Deck' 4 | 5 | export interface DecksSectionsQuery extends ParsedUrlQuery { 6 | unlockSectionId?: string 7 | } 8 | 9 | export interface DecksSectionsProps { 10 | deck: Deck 11 | } 12 | -------------------------------------------------------------------------------- /components/Dashboard/Decks/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/shadow'; 2 | @use 'styles/dashboard'; 3 | 4 | @include dashboard.sidebar-collapse(1080px); 5 | 6 | .content { 7 | display: grid; 8 | grid: auto 1fr / calc(100vw - var(--sidebar-width)); 9 | overflow: visible; 10 | } 11 | 12 | .main { 13 | display: grid; 14 | grid: 1fr / auto; 15 | overflow-y: auto; 16 | scroll-behavior: smooth; 17 | padding: 0 var(--horizontal-padding); 18 | } 19 | 20 | .box { 21 | @include shadow.large; 22 | 23 | width: calc(100vw - var(--sidebar-width) - var(--horizontal-padding) * 2); 24 | height: max-content; 25 | margin-bottom: var(--horizontal-padding); 26 | padding: var(--inner-horizontal-padding); 27 | background: white; 28 | border-radius: 8px; 29 | } 30 | 31 | .loading { 32 | display: grid; 33 | justify-content: center; 34 | align-content: center; 35 | height: auto; 36 | } 37 | -------------------------------------------------------------------------------- /components/Dashboard/Decks/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | export interface DecksQuery extends ParsedUrlQuery { 4 | slugId?: string 5 | slug?: string 6 | unlockSectionId?: string 7 | } 8 | -------------------------------------------------------------------------------- /components/Dashboard/EditCard/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | export interface EditCardQuery extends ParsedUrlQuery { 4 | slugId: string 5 | slug: string 6 | cardId: string 7 | } 8 | -------------------------------------------------------------------------------- /components/Dashboard/EditDeck/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | export interface EditDeckQuery extends ParsedUrlQuery { 4 | slugId: string 5 | slug: string 6 | } 7 | -------------------------------------------------------------------------------- /components/Dashboard/Interests/data.ts: -------------------------------------------------------------------------------- 1 | import { GetStaticProps } from 'next' 2 | 3 | import { InterestsProps } from './models' 4 | import getTopics from 'lib/getTopics' 5 | 6 | const REVALIDATE = 3600 // 1 hour 7 | 8 | export const getStaticProps: GetStaticProps< 9 | InterestsProps, 10 | Record 11 | > = async () => ({ 12 | props: { topics: await getTopics() }, 13 | revalidate: REVALIDATE 14 | }) 15 | -------------------------------------------------------------------------------- /components/Dashboard/Interests/models.ts: -------------------------------------------------------------------------------- 1 | import { TopicData } from 'models/Topic' 2 | 3 | export interface InterestsProps { 4 | topics: TopicData[] 5 | } 6 | -------------------------------------------------------------------------------- /components/Dashboard/Market/data.ts: -------------------------------------------------------------------------------- 1 | import { GetStaticProps } from 'next' 2 | 3 | import { MarketProps } from './models' 4 | import getNumberOfDecks from 'lib/getNumberOfDecks' 5 | 6 | const REVALIDATE = 3600 // 1 hour 7 | 8 | export const getStaticProps: GetStaticProps< 9 | MarketProps, 10 | Record 11 | > = async () => ({ 12 | props: { decks: await getNumberOfDecks() }, 13 | revalidate: REVALIDATE 14 | }) 15 | -------------------------------------------------------------------------------- /components/Dashboard/Market/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | export interface MarketQuery extends ParsedUrlQuery { 4 | q?: string 5 | s?: string 6 | } 7 | 8 | export interface MarketProps { 9 | decks: number 10 | } 11 | -------------------------------------------------------------------------------- /components/Dashboard/Navbar/Tab/index.module.scss: -------------------------------------------------------------------------------- 1 | $horizontal-padding: 20px; 2 | $bottom-padding: 13px; 3 | 4 | .root { 5 | display: flex; 6 | position: relative; 7 | align-items: center; 8 | padding: 0 $horizontal-padding $bottom-padding; 9 | color: white; 10 | opacity: 0.7; 11 | 12 | &:not(.selected):not(.disabled):hover { 13 | opacity: 0.9; 14 | } 15 | } 16 | 17 | .selected { 18 | opacity: 1; 19 | } 20 | 21 | .disabled { 22 | cursor: default; 23 | 24 | > svg, 25 | .title { 26 | opacity: 0.5; 27 | } 28 | } 29 | 30 | .hasOverlay:hover { 31 | opacity: 1; 32 | } 33 | 34 | .root, 35 | .root > svg, 36 | .title { 37 | transition: opacity 0.3s; 38 | } 39 | 40 | .overlay { 41 | position: absolute !important; 42 | top: 0; 43 | right: 0; 44 | bottom: $bottom-padding; 45 | left: 0; 46 | cursor: default !important; 47 | z-index: 1; 48 | } 49 | 50 | .root > svg { 51 | fill: white; 52 | } 53 | 54 | .title { 55 | margin-left: 10px; 56 | font-weight: bold; 57 | color: white; 58 | } 59 | -------------------------------------------------------------------------------- /components/Dashboard/Navbar/Tab/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import Link from 'next/link' 3 | import { UrlObject } from 'url' 4 | import cx from 'classnames' 5 | 6 | import styles from './index.module.scss' 7 | 8 | export interface DashboardNavbarTabProps { 9 | href: string | UrlObject 10 | title: string 11 | isSelected: boolean 12 | isDisabled: boolean 13 | message?: string 14 | children?: ReactNode 15 | } 16 | 17 | const DashboardNavbarTab = ({ 18 | href, 19 | title, 20 | isSelected, 21 | isDisabled, 22 | message, 23 | children 24 | }: DashboardNavbarTabProps) => ( 25 | 26 | isDisabled && event.preventDefault()} 33 | > 34 | {message && ( 35 | 40 | )} 41 | {children} 42 | {title} 43 | 44 | 45 | ) 46 | 47 | export default DashboardNavbarTab 48 | -------------------------------------------------------------------------------- /components/Dashboard/Review/Footer/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | $vertical-offset: 100px; 4 | 5 | .root { 6 | display: grid; 7 | justify-items: center; 8 | align-items: center; 9 | padding: 40px 0; 10 | } 11 | 12 | .waitingForRating { 13 | .message { 14 | opacity: 0; 15 | transform: translateY($vertical-offset); 16 | } 17 | 18 | .buttons { 19 | opacity: 1; 20 | transform: none; 21 | } 22 | } 23 | 24 | .message, 25 | .buttons { 26 | grid-row: 1; 27 | grid-column: 1; 28 | transition: opacity 0.3s, transform 0.3s; 29 | } 30 | 31 | .message { 32 | text-align: center; 33 | font-size: 20px; 34 | font-weight: 900; 35 | color: colors.$dark-gray; 36 | opacity: 0.8; 37 | } 38 | 39 | .buttons { 40 | display: flex; 41 | opacity: 0; 42 | transform: translateY($vertical-offset); 43 | } 44 | -------------------------------------------------------------------------------- /components/Dashboard/Review/Navbar/index.module.scss: -------------------------------------------------------------------------------- 1 | $button-height: 40px; 2 | 3 | .root { 4 | display: flex; 5 | align-items: center; 6 | overflow-x: auto; 7 | margin-top: 20px; 8 | } 9 | 10 | .back, 11 | .progress, 12 | .recap { 13 | flex-shrink: 0; 14 | } 15 | 16 | .back, 17 | .recap { 18 | height: $button-height; 19 | color: white; 20 | border: 2px solid transparentize(#eee, 0.8); 21 | border-radius: 8px; 22 | transition: opacity 0.3s, border-color 0.3s; 23 | 24 | &:hover { 25 | opacity: 0.5; 26 | border-color: transparentize(#eee, 0.6); 27 | } 28 | } 29 | 30 | .back { 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | width: $button-height; 35 | margin-right: 12px; 36 | 37 | &:hover .backIcon { 38 | transform: scale(1.7); 39 | } 40 | } 41 | 42 | .backIcon { 43 | transform: scale(1.5); 44 | transition: transform 0.3s; 45 | } 46 | 47 | .progress, 48 | .recap { 49 | font-weight: 900; 50 | } 51 | 52 | .progress { 53 | margin-right: auto; 54 | font-size: 25px; 55 | color: white; 56 | transform: translateY(-1px); 57 | } 58 | 59 | .recap { 60 | padding: 0 12px; 61 | text-transform: uppercase; 62 | font-size: 18px; 63 | } 64 | -------------------------------------------------------------------------------- /components/Dashboard/Review/RecapModal/index.module.scss: -------------------------------------------------------------------------------- 1 | $margin: 20px; 2 | 3 | .root { 4 | display: flex; 5 | flex-direction: column; 6 | max-width: 650px; 7 | width: calc(100vw - #{$margin * 2}); 8 | max-height: calc(100vh - #{$margin * 2}); 9 | overflow-y: auto; 10 | margin: $margin; 11 | padding: 20px; 12 | background: #051e34; 13 | border-radius: 15px; 14 | } 15 | 16 | .emoji { 17 | margin-bottom: -15px; 18 | text-align: center; 19 | font-size: 80px; 20 | } 21 | 22 | .data { 23 | color: white; 24 | } 25 | 26 | .deck { 27 | color: #ffcb6b; 28 | } 29 | 30 | .section { 31 | color: #c792ea; 32 | } 33 | 34 | .count { 35 | color: #89ddff; 36 | } 37 | 38 | .done { 39 | margin: 20px 0 0 auto; 40 | padding: 6px 20px; 41 | font-size: 20px; 42 | font-weight: 900; 43 | color: white; 44 | background: #0288d1; 45 | border-radius: 13px; 46 | transition: color 0.3s, background 0.3s; 47 | 48 | &:hover { 49 | color: #0288d1; 50 | background: white; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /components/Dashboard/Review/RecapModalData/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-top: 20px; 3 | } 4 | 5 | .title { 6 | text-transform: uppercase; 7 | font-weight: 900; 8 | color: white; 9 | } 10 | 11 | .content { 12 | font-weight: bold; 13 | color: transparentize(white, 0.5); 14 | } 15 | -------------------------------------------------------------------------------- /components/Dashboard/Review/RecapModalData/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | import styles from './index.module.scss' 4 | 5 | export interface ReviewRecapModalDataProps { 6 | title: string 7 | children?: ReactNode 8 | } 9 | 10 | const ReviewRecapModalData = ({ 11 | title, 12 | children 13 | }: ReviewRecapModalDataProps) => ( 14 |
15 |

{title}

16 |

{children}

17 |
18 | ) 19 | 20 | export default ReviewRecapModalData 21 | -------------------------------------------------------------------------------- /components/Dashboard/Review/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/dashboard'; 2 | 3 | @include dashboard.sidebar-collapse(980px); 4 | 5 | .content { 6 | display: flex; 7 | flex-direction: column; 8 | height: 100vh !important; 9 | overflow: hidden; 10 | padding: 0 var(--horizontal-padding); 11 | } 12 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/Contact/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | margin-top: 16px; 5 | color: colors.$dark-gray; 6 | } 7 | 8 | .title { 9 | font-weight: 900; 10 | } 11 | 12 | .description { 13 | margin-top: 4px; 14 | font-size: 15px; 15 | 16 | @media (min-width: 360px) { 17 | font-size: 16px; 18 | } 19 | 20 | @media (min-width: 600px) { 21 | font-size: 15px; 22 | } 23 | 24 | @media (min-width: 630px) { 25 | font-size: 16px; 26 | } 27 | } 28 | 29 | .action { 30 | font-weight: bold; 31 | color: #007aff; 32 | 33 | &:hover { 34 | text-decoration: underline; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/Contact/index.tsx: -------------------------------------------------------------------------------- 1 | import { SLACK_INVITE_URL } from 'lib/constants' 2 | 3 | import styles from './index.module.scss' 4 | 5 | const AccountSettingsContact = () => ( 6 |
7 |

Contact

8 |

9 | 15 | Join Slack 16 | {' '} 17 | or email{' '} 18 | 24 | support@memorize.ai 25 | 26 |

27 |
28 | ) 29 | 30 | export default AccountSettingsContact 31 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/Email/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | margin-top: 14px; 5 | } 6 | 7 | .label { 8 | font-weight: bold; 9 | color: colors.$dark-gray; 10 | } 11 | 12 | .text { 13 | max-width: 300px; 14 | overflow: hidden; 15 | margin-top: 4px; 16 | text-overflow: ellipsis; 17 | white-space: nowrap; 18 | color: colors.$dark-gray; 19 | 20 | &[aria-busy='true'] { 21 | opacity: 0.5; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/Email/index.tsx: -------------------------------------------------------------------------------- 1 | import useCurrentUser from 'hooks/useCurrentUser' 2 | 3 | import styles from './index.module.scss' 4 | 5 | const AccountSettingsEmail = () => { 6 | const [currentUser] = useCurrentUser() 7 | const email = currentUser?.email 8 | 9 | return ( 10 |
11 | 12 |

13 | {email ?? 'Loading...'} 14 |

15 |
16 | ) 17 | } 18 | 19 | export default AccountSettingsEmail 20 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/ForgotPassword/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | max-width: 300px; 6 | width: 100%; 7 | height: 40px; 8 | margin-top: 20px; 9 | font-weight: bold; 10 | color: #4355f9; 11 | border: 1px solid #ddd; 12 | border-radius: 8px; 13 | transition: color 0.3s, background 0.3s, border-color 0.3s; 14 | 15 | &:disabled { 16 | cursor: default; 17 | } 18 | 19 | &:not(:disabled) { 20 | &:hover, 21 | &:focus { 22 | $background: #051e34; 23 | 24 | color: white; 25 | background: $background; 26 | border-color: $background; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/Name/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | margin-top: 14px; 5 | padding-top: 14px; 6 | border-top: 1px solid #eee; 7 | } 8 | 9 | .header, 10 | .input { 11 | max-width: 300px; 12 | } 13 | 14 | .header { 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | margin-bottom: 4px; 19 | } 20 | 21 | .label { 22 | font-weight: bold; 23 | color: colors.$dark-gray; 24 | } 25 | 26 | .submit { 27 | font-weight: bold; 28 | color: #007aff; 29 | transition: opacity 0.3s; 30 | 31 | &:disabled, 32 | &:hover { 33 | opacity: 0.5; 34 | } 35 | } 36 | 37 | .loader { 38 | margin-right: 8px; 39 | } 40 | 41 | .input { 42 | width: 100%; 43 | padding: 8px 10px; 44 | color: colors.$dark-gray; 45 | background: transparentize(#f0f1f8, 1 / 3); 46 | border-radius: 8px; 47 | } 48 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/Profile/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | max-width: max-content; 5 | margin-top: 12px; 6 | font-weight: 900; 7 | color: #007aff; 8 | 9 | &:hover { 10 | text-decoration: underline; 11 | 12 | .icon { 13 | transform: translateX(4px); 14 | } 15 | } 16 | } 17 | 18 | .image, 19 | .defaultImage { 20 | $dimension: 30px; 21 | 22 | width: $dimension; 23 | height: $dimension; 24 | margin-right: 8px; 25 | border-radius: 50%; 26 | } 27 | 28 | .image { 29 | object-fit: cover; 30 | } 31 | 32 | .defaultImage { 33 | padding: 2px; 34 | fill: #007aff; 35 | border: 1px solid #eee; 36 | } 37 | 38 | .icon { 39 | margin-left: 4px; 40 | transition: transform 0.3s; 41 | } 42 | 43 | .loader { 44 | margin-left: 6px; 45 | } 46 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/SignOut/index.module.scss: -------------------------------------------------------------------------------- 1 | $margin: 20px; 2 | 3 | .root { 4 | margin-top: $margin; 5 | padding-top: $margin; 6 | border-top: 1px solid #eee; 7 | } 8 | 9 | .button { 10 | display: block; 11 | max-width: 360px; 12 | width: 100%; 13 | height: 40px; 14 | margin: 0 auto; 15 | font-weight: bold; 16 | color: white; 17 | background: #f55d23; 18 | border-radius: 8px; 19 | transition: background 0.3s; 20 | 21 | &:hover, 22 | &:hover { 23 | background: #051e34; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/SignOut/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent } from 'react' 2 | 3 | import firebase from 'lib/firebase' 4 | import handleError from 'lib/handleError' 5 | 6 | import styles from './index.module.scss' 7 | 8 | import 'firebase/auth' 9 | 10 | const auth = firebase.auth() 11 | 12 | const signOut = async (event: FormEvent) => { 13 | event.preventDefault() 14 | 15 | try { 16 | await auth.signOut() 17 | window.location.href = '/' 18 | } catch (error) { 19 | handleError(error) 20 | } 21 | } 22 | 23 | const AccountSettingsSignOut = () => ( 24 |
25 | 26 |
27 | ) 28 | 29 | export default AccountSettingsSignOut 30 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Account/index.tsx: -------------------------------------------------------------------------------- 1 | import Settings from '..' 2 | import Profile from './Profile' 3 | import Name from './Name' 4 | import Email from './Email' 5 | import ForgotPassword from './ForgotPassword' 6 | import Contact from './Contact' 7 | import SignOut from './SignOut' 8 | 9 | const AccountSettings = () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | 20 | export default AccountSettings 21 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Develop/Api/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | margin-top: 20px; 5 | padding-top: 12px; 6 | border-top: 1px solid #eee; 7 | } 8 | 9 | .title { 10 | font-weight: 900; 11 | color: colors.$dark-gray; 12 | } 13 | 14 | .subtitle { 15 | margin-bottom: 12px; 16 | font-size: 14px; 17 | color: colors.$dark-gray; 18 | 19 | @media (min-width: 300px) { 20 | font-size: 16px; 21 | } 22 | } 23 | 24 | .action { 25 | display: block; 26 | max-width: max-content; 27 | font-weight: 900; 28 | color: #007aff; 29 | 30 | &:hover .arrow { 31 | transform: translateX(4px); 32 | } 33 | 34 | & + & { 35 | margin-top: 8px; 36 | } 37 | } 38 | 39 | .icon { 40 | width: 18px !important; 41 | margin-right: 6px; 42 | } 43 | 44 | .arrow { 45 | margin-left: 4px; 46 | transition: transform 0.3s; 47 | } 48 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Develop/GitHub/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | $color: white; 4 | $background: #051e34; 5 | 6 | .root { 7 | display: inline-block; 8 | margin-top: 12px; 9 | padding: 6px 10px; 10 | font-weight: 900; 11 | color: $color; 12 | background: $background; 13 | border: 2px solid $background; 14 | border-radius: 8px; 15 | transition: color 0.3s, background 0.3s; 16 | 17 | &:hover { 18 | color: $background; 19 | background: $color; 20 | } 21 | } 22 | 23 | .icon { 24 | margin-right: 8px; 25 | font-size: 1.25em; 26 | vertical-align: -0.2em; 27 | } 28 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Develop/GitHub/index.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 2 | import { faGithub } from '@fortawesome/free-brands-svg-icons' 3 | 4 | import styles from './index.module.scss' 5 | 6 | const DeveloperSettingsGitHub = () => ( 7 | 13 | 14 | View on GitHub 15 | 16 | ) 17 | 18 | export default DeveloperSettingsGitHub 19 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Develop/index.tsx: -------------------------------------------------------------------------------- 1 | import Settings from '..' 2 | import GitHub from './GitHub' 3 | import Api from './Api' 4 | 5 | const DeveloperSettings = () => ( 6 | 7 | 8 | 9 | 10 | ) 11 | 12 | export default DeveloperSettings 13 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Navigation/Link/index.module.scss: -------------------------------------------------------------------------------- 1 | $color: #007aff; 2 | 3 | .root { 4 | display: flex; 5 | align-items: center; 6 | padding: 4px 8px; 7 | font-size: 16px; 8 | font-weight: bold; 9 | color: $color; 10 | border-radius: 8px; 11 | transition: font-weight 0.3s, background 0.3s; 12 | 13 | @media (min-width: 360px) { 14 | font-size: 18px; 15 | } 16 | 17 | @media (min-width: 600px) { 18 | font-size: 16px; 19 | } 20 | 21 | @media (min-width: 660px) { 22 | font-size: 18px; 23 | } 24 | 25 | &:hover { 26 | background: transparentize($color, 0.95); 27 | 28 | .arrow { 29 | opacity: 1; 30 | transform: translateX(4px); 31 | } 32 | } 33 | 34 | &[aria-current='page'] { 35 | pointer-events: none; 36 | font-weight: 900; 37 | background: transparentize($color, 0.9); 38 | } 39 | 40 | & + & { 41 | margin-top: 8px; 42 | } 43 | } 44 | 45 | .icon { 46 | width: 25px !important; 47 | height: 20px; 48 | margin-right: 8px; 49 | } 50 | 51 | .arrow { 52 | margin: 0 8px 0 auto; 53 | opacity: 0; 54 | transition: opacity 0.1s, transform 0.3s; 55 | } 56 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Navigation/Link/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import Link from 'next/link' 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | import { faChevronRight } from '@fortawesome/free-solid-svg-icons' 5 | import { IconDefinition } from '@fortawesome/fontawesome-svg-core' 6 | 7 | import styles from './index.module.scss' 8 | 9 | export interface SettingsNavigationLinkProps { 10 | current: string 11 | href: string 12 | icon: IconDefinition 13 | children?: ReactNode 14 | } 15 | 16 | const SettingsNavigationLink = ({ 17 | current, 18 | href, 19 | icon, 20 | children 21 | }: SettingsNavigationLinkProps) => ( 22 | 23 | 27 | 28 | {children} 29 | 30 | 31 | 32 | ) 33 | 34 | export default SettingsNavigationLink 35 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import { faBell, faCode, faUser } from '@fortawesome/free-solid-svg-icons' 3 | 4 | import Link from './Link' 5 | 6 | const SettingsNavigation = () => { 7 | const current = useRouter().asPath 8 | 9 | return ( 10 | 21 | ) 22 | } 23 | 24 | export default SettingsNavigation 25 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Notifications/index.module.scss: -------------------------------------------------------------------------------- 1 | .fixed { 2 | grid: auto auto / auto; 3 | 4 | @media (min-width: 870px) { 5 | grid: auto / auto 1fr; 6 | } 7 | } 8 | 9 | .day { 10 | padding: 0 6px; 11 | font-size: 12px; 12 | 13 | @media (min-width: 665px) { 14 | padding: 0 8px; 15 | } 16 | 17 | @media (min-width: 750px) { 18 | padding: 0 12px; 19 | font-size: 14px; 20 | } 21 | } 22 | 23 | .timeTrigger { 24 | font-size: 14px; 25 | 26 | @media (min-width: 750px) { 27 | font-size: 16px; 28 | } 29 | } 30 | 31 | .timeContent { 32 | max-height: 300px; 33 | left: 0; 34 | right: unset; 35 | 36 | @media (min-width: 600px) { 37 | max-height: calc(100vh - 480px); 38 | } 39 | 40 | @media (min-width: 610px) { 41 | max-height: calc(100vh - 460px); 42 | } 43 | 44 | @media (min-width: 735px) { 45 | max-height: calc(100vh - 440px); 46 | } 47 | 48 | @media (min-width: 870px) { 49 | max-height: calc(100vh - 400px); 50 | left: unset; 51 | right: 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/Notifications/index.tsx: -------------------------------------------------------------------------------- 1 | import Settings from '..' 2 | import Notifications from 'components/Notifications' 3 | 4 | import styles from './index.module.scss' 5 | 6 | const ID_PREFIX = 'notification-settings' 7 | 8 | const NotificationSettings = () => ( 9 | 10 | 17 | 18 | ) 19 | 20 | export default NotificationSettings 21 | -------------------------------------------------------------------------------- /components/Dashboard/Settings/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | @use 'styles/shadow'; 3 | @use 'styles/dashboard'; 4 | 5 | @include dashboard.sidebar-collapse(1200px); 6 | 7 | .container { 8 | scroll-behavior: smooth; 9 | padding: 0 var(--horizontal-padding) var(--horizontal-padding); 10 | } 11 | 12 | .titleIcon { 13 | margin-right: 12px; 14 | } 15 | 16 | .title { 17 | margin: 12px 0 16px; 18 | font-weight: 900; 19 | color: white; 20 | } 21 | 22 | .content { 23 | --padding: 16px; 24 | 25 | @include shadow.large; 26 | 27 | display: grid; 28 | grid: auto 1fr / 1fr; 29 | padding: var(--padding); 30 | background: white; 31 | border-radius: 8px; 32 | 33 | @media (min-width: 450px) { 34 | --padding: 20px; 35 | } 36 | 37 | @media (min-width: 600px) { 38 | --padding: 16px; 39 | 40 | grid: 1fr / 180px 1fr; 41 | } 42 | 43 | @media (min-width: 660px) { 44 | --padding: 20px; 45 | 46 | grid: 1fr / 200px 1fr; 47 | } 48 | } 49 | 50 | .main { 51 | margin-top: var(--padding); 52 | 53 | @media (min-width: 600px) { 54 | margin-top: 0; 55 | margin-left: var(--padding); 56 | padding-left: var(--padding); 57 | border-left: 1px solid #eee; 58 | } 59 | } 60 | 61 | .name { 62 | font-weight: 900; 63 | color: colors.$dark-gray; 64 | } 65 | -------------------------------------------------------------------------------- /components/Dashboard/SidebarRow/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | padding: 8px 20px; 5 | transition: background 0.3s; 6 | 7 | &:not(.selected) { 8 | &:hover, 9 | &:focus { 10 | background: transparentize(#582efe, 0.5); 11 | } 12 | } 13 | 14 | &:not(:last-child) { 15 | margin-bottom: 4px; 16 | } 17 | } 18 | 19 | .selected { 20 | background: #582efe; 21 | 22 | .title { 23 | font-weight: bold; 24 | opacity: 1; 25 | } 26 | } 27 | 28 | .image, 29 | .badge { 30 | flex-shrink: 0; 31 | } 32 | 33 | .image { 34 | $dimension: 50px; 35 | 36 | width: $dimension; 37 | height: $dimension; 38 | object-fit: cover; 39 | background: white; 40 | border-radius: 5px; 41 | } 42 | 43 | .title { 44 | margin: 0 12px; 45 | color: white; 46 | opacity: 0.8; 47 | transition: color 0.3s, opacity 0.3s; 48 | } 49 | 50 | .badge { 51 | $min-width: 35px; 52 | $height: 25px; 53 | $horizontal-padding: 8px; 54 | 55 | min-width: $min-width; 56 | height: $height; 57 | margin-left: auto; 58 | padding: 0 $horizontal-padding; 59 | text-align: center; 60 | font-weight: bold; 61 | line-height: $height; 62 | color: white; 63 | background: #00d388; 64 | border-radius: 4px; 65 | } 66 | -------------------------------------------------------------------------------- /components/Dashboard/SidebarRow/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import cx from 'classnames' 3 | 4 | import Deck from 'models/Deck' 5 | import useSelectedDeck from 'hooks/useSelectedDeck' 6 | import formatNumber from 'lib/formatNumber' 7 | 8 | import { src as defaultImage } from 'images/defaults/deck.jpg' 9 | import styles from './index.module.scss' 10 | 11 | export interface DashboardSidebarRowProps { 12 | deck: Deck 13 | } 14 | 15 | const DashboardSidebarRow = ({ deck }: DashboardSidebarRowProps) => { 16 | const [selectedDeck] = useSelectedDeck() 17 | 18 | const numberOfDueCards = deck.userData?.numberOfDueCards ?? 0 19 | 20 | return ( 21 | 22 | 27 | {deck.name} 33 | {deck.name} 34 | {numberOfDueCards > 0 && ( 35 | {formatNumber(numberOfDueCards)} 36 | )} 37 | 38 | 39 | ) 40 | } 41 | 42 | export default DashboardSidebarRow 43 | -------------------------------------------------------------------------------- /components/Dashboard/SidebarSection/index.module.scss: -------------------------------------------------------------------------------- 1 | $divider-color: transparentize(#ddd, 0.8); 2 | 3 | @mixin divider($height) { 4 | height: $height; 5 | background: $divider-color; 6 | border-radius: $height / 2; 7 | } 8 | 9 | .root { 10 | margin-top: 16px; 11 | 12 | &:first-child { 13 | margin-top: 4px; 14 | } 15 | 16 | &:last-child { 17 | margin-bottom: 12px; 18 | } 19 | } 20 | 21 | .title { 22 | margin: 0 20px 8px 20px; 23 | font-weight: bold; 24 | color: white; 25 | } 26 | 27 | .divider { 28 | @include divider(2.5px); 29 | 30 | margin: 16px 20px 0 20px; 31 | } 32 | -------------------------------------------------------------------------------- /components/Dashboard/SidebarSection/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import Deck from 'models/Deck' 4 | import includesNormalized from 'lib/includesNormalized' 5 | import Row from '../SidebarRow' 6 | 7 | import styles from './index.module.scss' 8 | 9 | export interface DashboardSidebarSectionProps { 10 | title: string 11 | decks: Deck[] 12 | query: string 13 | includesDivider?: boolean 14 | } 15 | 16 | const DashboardSidebarSection = ({ 17 | title, 18 | decks: allDecks, 19 | query, 20 | includesDivider = false 21 | }: DashboardSidebarSectionProps) => { 22 | const decks = useMemo( 23 | () => 24 | allDecks.filter( 25 | deck => deck.name && includesNormalized(query, [deck.name]) 26 | ), 27 | [allDecks, query] 28 | ) 29 | 30 | return decks.length ? ( 31 |
32 |

{title}

33 |
34 | {decks.map(deck => ( 35 | 36 | ))} 37 |
38 | {includesDivider &&
} 39 |
40 | ) : null 41 | } 42 | 43 | export default DashboardSidebarSection 44 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/Activity/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .title { 4 | font-size: 24px; 5 | font-weight: 900; 6 | color: colors.$dark-gray; 7 | } 8 | 9 | .content { 10 | display: flex; 11 | justify-content: flex-end; 12 | max-width: max-content; 13 | overflow: hidden; 14 | margin-top: 8px; 15 | padding: 12px 20px 20px 12px; 16 | border: 1px solid #eee; 17 | border-radius: 8px; 18 | } 19 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/Activity/index.tsx: -------------------------------------------------------------------------------- 1 | import User from 'models/User' 2 | import { ActivityNodeData } from 'models/ActivityNode' 3 | import Activity from 'components/Activity' 4 | 5 | import styles from './index.module.scss' 6 | 7 | export interface UserPageActivityProps { 8 | user: User 9 | activity: Record 10 | } 11 | 12 | const UserPageActivity = ({ user, activity }: UserPageActivityProps) => ( 13 |
14 |

Activity

15 |
16 | 17 |
18 |
19 | ) 20 | 21 | export default UserPageActivity 22 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/Bio/index.tsx: -------------------------------------------------------------------------------- 1 | import User from 'models/User' 2 | 3 | import styles from './index.module.scss' 4 | 5 | export interface UserPageBioProps { 6 | user: User 7 | } 8 | 9 | const UserPageBio = ({ user }: UserPageBioProps) => ( 10 |
11 |

About

12 |
16 |
17 | ) 18 | 19 | export default UserPageBio 20 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/Contact/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | height: 40px; 5 | margin-top: 22px; 6 | padding: 0 30px; 7 | text-transform: uppercase; 8 | font-size: 18px; 9 | font-weight: 900; 10 | color: white; 11 | background: #f85ea1; 12 | border-radius: 8px; 13 | transition: background 0.3s; 14 | 15 | &:not(:disabled):hover { 16 | background: #051e34; 17 | } 18 | } 19 | 20 | .loader, 21 | .icon { 22 | margin-right: 10px; 23 | } 24 | 25 | .icon { 26 | transform: scale(1.2); 27 | } 28 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/Decks/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | margin-bottom: 14px; 5 | } 6 | 7 | .title { 8 | margin: 0 var(--inner-horizontal-padding); 9 | font-size: 20px; 10 | font-weight: 900; 11 | color: colors.$dark-gray; 12 | 13 | @media (min-width: 450px) { 14 | padding-top: 20px; 15 | font-size: 24px; 16 | border-top: 1px solid #eee; 17 | } 18 | } 19 | 20 | .count { 21 | display: none; 22 | color: #582efe; 23 | 24 | @media (min-width: 365px) { 25 | display: inline; 26 | } 27 | } 28 | 29 | .rows { 30 | $margin-top: 12px; 31 | $padding-top: 4px; 32 | 33 | overflow-x: auto; 34 | margin-top: $margin-top - $padding-top; 35 | padding: $padding-top var(--inner-horizontal-padding) 16px; 36 | } 37 | 38 | .row { 39 | display: flex; 40 | 41 | &:not(:last-child) { 42 | margin-bottom: 12px; 43 | } 44 | } 45 | 46 | .deck { 47 | flex-shrink: 0; 48 | 49 | &:not(:last-child) { 50 | margin-right: 12px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/EditBio/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | margin-bottom: 20px; 5 | } 6 | 7 | .header { 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: flex-end; 11 | margin-bottom: 12px; 12 | } 13 | 14 | .title { 15 | margin: 0 12px -4px 0; 16 | font-size: 24px; 17 | font-weight: 900; 18 | color: colors.$dark-gray; 19 | } 20 | 21 | .save { 22 | display: grid; 23 | justify-items: center; 24 | align-items: center; 25 | width: 80px; 26 | height: 40px; 27 | font-size: 17px; 28 | font-weight: 900; 29 | color: white; 30 | background: #007aff; 31 | text-transform: uppercase; 32 | border-radius: 8px; 33 | transition: background 0.3s, opacity 0.3s; 34 | 35 | &:not(:disabled) { 36 | &:hover, 37 | &:focus { 38 | background: #051e34; 39 | } 40 | } 41 | } 42 | 43 | .saveDisabled { 44 | opacity: 0.5; 45 | } 46 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/Level/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .stats { 4 | margin-bottom: 4px; 5 | } 6 | 7 | .level { 8 | font-weight: bold; 9 | color: colors.$dark-gray; 10 | } 11 | 12 | .bullet, 13 | .xp { 14 | color: transparentize(colors.$dark-gray, 0.5); 15 | } 16 | 17 | .bullet { 18 | opacity: 0.7; 19 | } 20 | 21 | .sliderContainer { 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | .slider, 27 | .sliderContent { 28 | $height: 12px; 29 | 30 | height: $height; 31 | border-radius: $height / 2; 32 | } 33 | 34 | .slider { 35 | width: 100%; 36 | overflow: hidden; 37 | margin-right: 20px; 38 | background: transparentize(colors.$dark-gray, 0.9); 39 | } 40 | 41 | .sliderContent { 42 | background: #00d388; 43 | transition: width 0.3s ease-in-out; 44 | } 45 | 46 | .sliderValue { 47 | white-space: nowrap; 48 | font-weight: bold; 49 | color: colors.$dark-gray; 50 | } 51 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/Level/index.tsx: -------------------------------------------------------------------------------- 1 | import User from 'models/User' 2 | import formatNumber, { formatNumberAsInt } from 'lib/formatNumber' 3 | 4 | import styles from './index.module.scss' 5 | 6 | export interface UserPageLevelProps { 7 | user: User 8 | } 9 | 10 | const UserPageLevel = ({ user }: UserPageLevelProps) => ( 11 |
12 |

13 | 14 | lvl {formatNumberAsInt(user.level ?? 0)} 15 | {' '} 16 | {' '} 17 | {formatNumber(user.xp ?? 0)} xp 18 |

19 |
20 |
21 |
25 |
26 |

27 | lvl {formatNumberAsInt((user.level ?? 0) + 1)} 28 |

29 |
30 |
31 | ) 32 | 33 | export default UserPageLevel 34 | -------------------------------------------------------------------------------- /components/Dashboard/UserPage/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | import UserData from 'models/User/Data' 4 | import { DeckData } from 'models/Deck' 5 | import { ActivityNodeData } from 'models/ActivityNode' 6 | 7 | export interface UserPageQuery extends ParsedUrlQuery { 8 | slugId: string 9 | slug: string 10 | } 11 | 12 | export interface UserPageProps { 13 | user: UserData 14 | activity: Record 15 | decks: DeckData[] 16 | bio: string 17 | } 18 | 19 | export interface UserPagePath { 20 | params: UserPageQuery 21 | } 22 | -------------------------------------------------------------------------------- /components/Dashboard/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/gradient'; 2 | 3 | .root { 4 | --sidebar-width: 305px; 5 | --horizontal-padding: 8px; 6 | --inner-horizontal-padding: 16px; 7 | 8 | display: grid; 9 | grid: 10 | 'sidebar content' 1fr / 11 | var(--sidebar-width) 1fr; 12 | height: 100vh; 13 | 14 | @media (min-width: 450px) { 15 | --horizontal-padding: 30px; 16 | --inner-horizontal-padding: 30px; 17 | } 18 | } 19 | 20 | .content { 21 | grid-area: content; 22 | display: grid; 23 | } 24 | 25 | .background, 26 | .container { 27 | grid-row: 1; 28 | grid-column: 1; 29 | } 30 | 31 | .background { 32 | height: 500px; 33 | } 34 | 35 | .gradient_blue { 36 | @include gradient.top(6deg, $is-right: false); 37 | } 38 | 39 | .gradient_green { 40 | @include gradient.top( 41 | 6deg, 42 | $is-right: false, 43 | $top-color: #06ba7a, 44 | $bottom-color: #73d63f 45 | ); 46 | } 47 | 48 | .container { 49 | --navbar-height: 71px; 50 | 51 | display: grid; 52 | grid: var(--navbar-height) 1fr / 1fr; 53 | height: 100vh; 54 | z-index: 10; 55 | } 56 | 57 | .hiddenNavbar { 58 | --navbar-height: 0; 59 | 60 | .navbar { 61 | display: none; 62 | } 63 | } 64 | 65 | .foreground { 66 | height: calc(100vh - var(--navbar-height)); 67 | overflow-y: auto; 68 | } 69 | -------------------------------------------------------------------------------- /components/DeckCell/Owned/index.module.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .due { 7 | margin-top: auto; 8 | font-weight: bold; 9 | color: #9b9b9b; 10 | } 11 | 12 | .dueEmoji { 13 | display: inline-block; 14 | margin-right: 2px; 15 | font-size: 1.4em; 16 | transform: translateY(2px); 17 | } 18 | 19 | .review { 20 | margin-top: 10px; 21 | padding: 4px 0; 22 | text-align: center; 23 | font-size: 18px; 24 | font-weight: 900; 25 | color: white; 26 | background: #582efe; 27 | text-transform: uppercase; 28 | border-radius: 8px; 29 | transition: background 0.3s; 30 | 31 | &:hover, 32 | &:focus { 33 | background: #051e34; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/DeckCell/index.module.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .stats { 7 | display: flex; 8 | align-items: center; 9 | margin-top: auto; 10 | } 11 | 12 | .divider { 13 | width: 2px; 14 | height: 25px; 15 | margin: 0 20px; 16 | background: transparentize(#ddd, 0.5); 17 | } 18 | 19 | .rating, 20 | .downloads, 21 | .users { 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | .icon { 27 | margin-right: 4px; 28 | transform: scale(1.7); 29 | } 30 | 31 | .text { 32 | margin-left: 8px; 33 | font-size: 17px; 34 | font-weight: 600; 35 | color: #9b9b9b; 36 | } 37 | 38 | .action { 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | width: 100%; 43 | height: 30px; 44 | margin-top: 18px; 45 | font-weight: bold; 46 | text-transform: uppercase; 47 | color: white; 48 | border-radius: 8px; 49 | transition: color 0.3s, background 0.3s; 50 | } 51 | 52 | .get { 53 | background: #4355f9; 54 | 55 | &:hover, 56 | &:focus { 57 | background: #00d388; 58 | } 59 | } 60 | 61 | .open { 62 | background: #00d388; 63 | 64 | &:hover, 65 | &:focus { 66 | background: #051e34; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /components/Disqus/CommentCount/index.tsx: -------------------------------------------------------------------------------- 1 | import { CommentCount } from 'disqus-react' 2 | 3 | import { DisqusProps, componentProps } from '..' 4 | 5 | const DisqusCommentCount = (props: DisqusProps) => ( 6 | 7 | ) 8 | 9 | export default DisqusCommentCount 10 | -------------------------------------------------------------------------------- /components/Disqus/DiscussionEmbed/index.tsx: -------------------------------------------------------------------------------- 1 | import { DiscussionEmbed } from 'disqus-react' 2 | 3 | import { DisqusProps, componentProps } from '..' 4 | 5 | const DisqusDiscussionEmbed = (props: DisqusProps) => ( 6 | 7 | ) 8 | 9 | export default DisqusDiscussionEmbed 10 | -------------------------------------------------------------------------------- /components/Disqus/index.tsx: -------------------------------------------------------------------------------- 1 | import { DISQUS_SHORTNAME } from 'lib/constants' 2 | 3 | export interface DisqusProps { 4 | url: string 5 | id: string 6 | title: string 7 | } 8 | 9 | export const configFromProps = ({ url, id, title }: DisqusProps) => ({ 10 | url, 11 | identifier: id, 12 | title 13 | }) 14 | 15 | export const componentProps = (props: DisqusProps) => ({ 16 | shortname: DISQUS_SHORTNAME, 17 | config: configFromProps(props) 18 | }) 19 | -------------------------------------------------------------------------------- /components/Dropdown/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/z-index'; 2 | 3 | .root { 4 | position: relative; 5 | 6 | &::before { 7 | position: fixed; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | z-index: z-index.$dropdown; 13 | } 14 | } 15 | 16 | .showing::before { 17 | content: ''; 18 | } 19 | 20 | .content { 21 | position: absolute; 22 | pointer-events: none; 23 | top: 100%; 24 | left: 0; 25 | margin-top: 8px; 26 | transform: translateX(-20px); 27 | opacity: 0; 28 | z-index: z-index.$dropdown; 29 | transition: transform 0.2s, opacity 0.2s; 30 | 31 | &[aria-hidden='false'] { 32 | pointer-events: all; 33 | transform: none; 34 | opacity: 1; 35 | } 36 | } 37 | 38 | .right { 39 | left: unset; 40 | right: 0; 41 | } 42 | 43 | .shadow_around { 44 | border: 1px solid #eee; 45 | box-shadow: 0 0 10px 2px transparentize(black, 0.9); 46 | } 47 | 48 | .shadow_screen { 49 | box-shadow: 0 0 0 100vmax transparentize(black, 0.7); 50 | } 51 | -------------------------------------------------------------------------------- /components/Home/AuthButton/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | height: 44px; 3 | padding: 0 14px; 4 | font-size: larger; 5 | font-weight: 900; 6 | color: white; 7 | background: #051e34; 8 | border-radius: 8px; 9 | transition: color 0.3s, background 0.3s, transform 0.3s; 10 | 11 | &:hover, 12 | &:focus { 13 | color: #051e34; 14 | background: white; 15 | } 16 | } 17 | 18 | .slash { 19 | opacity: 0.7; 20 | } 21 | -------------------------------------------------------------------------------- /components/Home/AuthButton/index.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames' 2 | 3 | import AuthButton from 'components/AuthButton' 4 | 5 | import styles from './index.module.scss' 6 | 7 | export interface AuthButtonProps { 8 | className?: string 9 | } 10 | 11 | const HomeAuthButton = ({ className }: AuthButtonProps) => ( 12 | 13 | Log in / Sign up 14 | 15 | ) 16 | 17 | export default HomeAuthButton 18 | -------------------------------------------------------------------------------- /components/Home/Classroom/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Img, { Svg } from 'react-optimized-image' 3 | 4 | import List from './List' 5 | 6 | import leftArrow from 'images/icons/left-arrow.svg' 7 | import diagram from 'images/home/classroom.png' 8 | import styles from './index.module.scss' 9 | 10 | const HomeClassroom = () => ( 11 |
12 | Classroom diagram 18 | 32 |
33 | ) 34 | 35 | export default HomeClassroom 36 | -------------------------------------------------------------------------------- /components/Home/Preview/ClaimXPButton/index.module.scss: -------------------------------------------------------------------------------- 1 | $height: 44px; 2 | 3 | .root { 4 | padding: 0 14px; 5 | line-height: $height; 6 | font-size: larger; 7 | font-weight: 900; 8 | color: white; 9 | background: #051e34; 10 | border-radius: 8px; 11 | transition: color 0.3s, background 0.3s, opacity 0.3s; 12 | 13 | &:hover, 14 | &:focus { 15 | color: #051e34; 16 | background: white; 17 | } 18 | } 19 | 20 | .inverted { 21 | color: #051e34; 22 | background: white; 23 | box-shadow: 0 4px 8px transparentize(black, 0.9); 24 | 25 | &:hover, 26 | &:focus { 27 | color: white; 28 | background: #051e34; 29 | } 30 | } 31 | 32 | .xp { 33 | color: #03a9f4; 34 | } 35 | -------------------------------------------------------------------------------- /components/Home/Preview/ClaimXPButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react' 2 | import cx from 'classnames' 3 | 4 | import useAuthModal from 'hooks/useAuthModal' 5 | import AuthButton from 'components/AuthButton' 6 | 7 | import styles from './index.module.scss' 8 | 9 | export interface PreviewClaimXPButtonProps 10 | extends ButtonHTMLAttributes { 11 | inverted?: boolean 12 | } 13 | 14 | const PreviewClaimXPButton = ({ 15 | className, 16 | inverted = false, 17 | ...props 18 | }: PreviewClaimXPButtonProps) => { 19 | const { initialXp } = useAuthModal() 20 | 21 | return ( 22 | 30 | {initialXp > 0 ? ( 31 | <> 32 | Claim {initialXp} xp 33 | 34 | ) : ( 35 | 'Start learning' 36 | )} 37 | 38 | ) 39 | } 40 | 41 | export default PreviewClaimXPButton 42 | -------------------------------------------------------------------------------- /components/Home/Preview/Footer/index.module.scss: -------------------------------------------------------------------------------- 1 | $vertical-offset: 100px; 2 | 3 | .root { 4 | display: grid; 5 | justify-items: center; 6 | align-items: center; 7 | margin-top: 40px; 8 | } 9 | 10 | .finished { 11 | .claim { 12 | pointer-events: all; 13 | opacity: 1; 14 | } 15 | 16 | .message, 17 | .buttons { 18 | pointer-events: none; 19 | opacity: 0; 20 | } 21 | } 22 | 23 | .waitingForRating { 24 | .message { 25 | opacity: 0; 26 | transform: translateY($vertical-offset); 27 | } 28 | 29 | .buttons { 30 | pointer-events: all; 31 | opacity: 1; 32 | transform: none; 33 | } 34 | } 35 | 36 | .claim, 37 | .message, 38 | .buttons { 39 | grid-row: 1; 40 | grid-column: 1; 41 | } 42 | 43 | .claim { 44 | max-width: 350px; 45 | width: 100%; 46 | pointer-events: none; 47 | opacity: 0; 48 | } 49 | 50 | .message, 51 | .buttons { 52 | transition: opacity 0.3s, transform 0.3s; 53 | } 54 | 55 | .message { 56 | text-align: center; 57 | font-size: 20px; 58 | font-weight: 900; 59 | color: white; 60 | } 61 | 62 | .buttons { 63 | display: flex; 64 | justify-content: center; 65 | opacity: 0; 66 | pointer-events: none; 67 | transform: translateY($vertical-offset); 68 | } 69 | -------------------------------------------------------------------------------- /components/Home/SectionDivider/index.module.scss: -------------------------------------------------------------------------------- 1 | $degrees: 6deg; 2 | 3 | .root { 4 | height: 50px; 5 | transform: skewY($degrees); 6 | transform-origin: top right; 7 | } 8 | -------------------------------------------------------------------------------- /components/Home/SectionDivider/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.scss' 2 | 3 | const HomeSectionDivider = () =>
4 | 5 | export default HomeSectionDivider 6 | -------------------------------------------------------------------------------- /components/Home/SpacedRepetition/index.tsx: -------------------------------------------------------------------------------- 1 | import Img from 'react-optimized-image' 2 | 3 | import spacedRepetition from 'images/home/spaced-repetition.png' 4 | import styles from './index.module.scss' 5 | 6 | const HomeSpacedRepetition = () => ( 7 |
8 | Spaced Repetition diagram 14 |
15 |

16 | Spaced Repetition 17 |
18 | with AI 19 |

20 |

21 | Tired of long study sessions? Memorization is strongest when 22 | timing it just right. Try to recall too early and it 23 | won't stick. Too late and you'll forget. Struggle a little to remember 24 | and you won't forget! We use artificial intelligence to get this spacing{' '} 25 | perfect. 26 |

27 |
28 |
29 | ) 30 | 31 | export default HomeSpacedRepetition 32 | -------------------------------------------------------------------------------- /components/Home/WhiteArrowAuthButton/index.module.scss: -------------------------------------------------------------------------------- 1 | $icon-scale-multiplier: 1.2; 2 | $icon-scale: scale(-$icon-scale-multiplier, $icon-scale-multiplier); 3 | 4 | .root { 5 | flex-shrink: 0; 6 | display: flex; 7 | align-items: center; 8 | margin-right: 16px; 9 | padding: 8px 14px; 10 | color: #051e34; 11 | background: white; 12 | border-radius: 8px; 13 | box-shadow: 0 4px 8px transparentize(black, 0.9); 14 | transition: color 0.3s, background 0.3s, box-shadow 0.3s; 15 | 16 | &:hover, 17 | &:focus { 18 | color: white; 19 | background: #051e34; 20 | box-shadow: none; 21 | 22 | .icon { 23 | transform: $icon-scale rotate(-1turn); 24 | } 25 | } 26 | } 27 | 28 | .text { 29 | font-size: 20px; 30 | font-weight: 900; 31 | } 32 | 33 | .icon { 34 | margin: 0 2px 0 12px; 35 | transform: $icon-scale; 36 | transition: transform 0.3s; 37 | } 38 | -------------------------------------------------------------------------------- /components/Home/WhiteArrowAuthButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react' 2 | import { Svg } from 'react-optimized-image' 3 | import cx from 'classnames' 4 | 5 | import AuthButton from 'components/AuthButton' 6 | 7 | import leftArrow from 'images/icons/left-arrow.svg' 8 | import styles from './index.module.scss' 9 | 10 | export type WhiteArrowAuthButtonProps = ButtonHTMLAttributes 11 | 12 | const WhiteArrowAuthButton = ({ 13 | children, 14 | className, 15 | ...props 16 | }: WhiteArrowAuthButtonProps) => ( 17 | 23 | {children} 24 | 25 | 26 | ) 27 | 28 | export default WhiteArrowAuthButton 29 | -------------------------------------------------------------------------------- /components/Home/index.module.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | $horizontal-padding: 135px; 3 | 4 | padding: 0 20px; 5 | 6 | @media (min-width: 350px) { 7 | padding: 0 30px; 8 | } 9 | 10 | @media (min-width: 400px) { 11 | padding: 0 40px; 12 | } 13 | 14 | @media (min-width: 1100px) { 15 | padding: 0 ($horizontal-padding / 2); 16 | } 17 | 18 | @media (min-width: 1250px) { 19 | padding: 0 $horizontal-padding; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/Input/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | position: relative; 5 | } 6 | 7 | .input { 8 | width: 100%; 9 | padding: 6px 6px 6px 2.25em; 10 | color: colors.$dark-gray; 11 | border: 2px solid colors.$gray-300; 12 | border-radius: 0.25rem; 13 | outline: none; 14 | 15 | &:hover { 16 | border-color: colors.$gray-400; 17 | 18 | + .icon { 19 | color: colors.$gray-400; 20 | } 21 | } 22 | 23 | &:focus { 24 | border-color: colors.$blue-400; 25 | 26 | + .icon { 27 | color: colors.$blue-400; 28 | } 29 | } 30 | } 31 | 32 | .icon { 33 | position: absolute; 34 | left: 12px; 35 | top: 12px; 36 | pointer-events: none; 37 | color: colors.$gray-300; 38 | } 39 | -------------------------------------------------------------------------------- /components/Landing/data.ts: -------------------------------------------------------------------------------- 1 | import { GetStaticProps } from 'next' 2 | 3 | import getPreviewDeck from 'lib/getPreviewDeck' 4 | import { HomeProps } from 'components/Home' 5 | 6 | const REVALIDATE = 3600 // 1 hour 7 | 8 | export const getStaticProps: GetStaticProps< 9 | HomeProps, 10 | Record 11 | > = async () => ({ 12 | props: { previewDeck: await getPreviewDeck() }, 13 | revalidate: REVALIDATE 14 | }) 15 | -------------------------------------------------------------------------------- /components/Landing/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from 'components/Home' 2 | -------------------------------------------------------------------------------- /components/Loader/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: block; 3 | border-radius: 50%; 4 | animation: spin 0.8s linear infinite; 5 | } 6 | 7 | @keyframes spin { 8 | to { 9 | transform: rotate(1turn); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react' 2 | import cx from 'classnames' 3 | 4 | import styles from './index.module.scss' 5 | 6 | export interface LoaderProps extends HTMLAttributes { 7 | size: string 8 | thickness: string 9 | color: string 10 | } 11 | 12 | const Loader = ({ 13 | className, 14 | size, 15 | thickness, 16 | color, 17 | ...props 18 | }: LoaderProps) => ( 19 | 30 | ) 31 | 32 | export default Loader 33 | -------------------------------------------------------------------------------- /components/MarketSearchLink/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | max-width: 250px; 4 | height: 44px; 5 | overflow: hidden; 6 | border-radius: 8px; 7 | } 8 | 9 | .root, 10 | .input { 11 | width: 100%; 12 | } 13 | 14 | .input { 15 | height: 100%; 16 | padding: 0 12px 0 40px; 17 | font-weight: bold; 18 | color: white; 19 | background: transparentize(#eee, 0.8); 20 | 21 | &::placeholder { 22 | color: transparentize(#eee, 0.2); 23 | } 24 | } 25 | 26 | .icon { 27 | position: absolute; 28 | top: 14px; 29 | left: 14px; 30 | color: #eee; 31 | pointer-events: none; 32 | } 33 | -------------------------------------------------------------------------------- /components/MarketSearchLink/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { useRecoilValue } from 'recoil' 3 | import Router from 'next/router' 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 5 | import { faSearch } from '@fortawesome/free-solid-svg-icons' 6 | import cx from 'classnames' 7 | 8 | import searchState from 'state/search' 9 | import useUrlForMarket from 'hooks/useUrlForMarket' 10 | import { DEFAULT_DECK_COUNT } from 'lib/constants' 11 | 12 | import styles from './index.module.scss' 13 | 14 | export interface MarketSearchLinkProps { 15 | className?: string 16 | } 17 | 18 | const MarketSearchLink = ({ className }: MarketSearchLinkProps) => { 19 | const { query } = useRecoilValue(searchState) 20 | const marketUrl = useUrlForMarket() 21 | 22 | const goToMarket = useCallback(() => { 23 | Router.push(marketUrl) 24 | }, [marketUrl]) 25 | 26 | return ( 27 |
28 | 34 | 35 |
36 | ) 37 | } 38 | 39 | export default MarketSearchLink 40 | -------------------------------------------------------------------------------- /components/Modal/ApiKey/index.tsx: -------------------------------------------------------------------------------- 1 | import { faKey } from '@fortawesome/free-solid-svg-icons' 2 | 3 | import { ModalShowingProps } from '..' 4 | import CopyModal from '../Copy' 5 | 6 | export interface ApiKeyModalProps extends ModalShowingProps { 7 | value: string 8 | } 9 | 10 | const ApiKeyModal = ({ value, isShowing, setIsShowing }: ApiKeyModalProps) => ( 11 | 18 | ) 19 | 20 | export default ApiKeyModal 21 | -------------------------------------------------------------------------------- /components/Modal/Auth/Providers/index.module.scss: -------------------------------------------------------------------------------- 1 | $footer-height: 43px; 2 | 3 | .root { 4 | display: flex; 5 | } 6 | 7 | .button { 8 | display: flex; 9 | align-items: center; 10 | height: $footer-height; 11 | padding: 9px 12px; 12 | color: white; 13 | background: #051e34; 14 | border-radius: 8px; 15 | box-shadow: 0 0 20px 5px transparentize(black, 0.9); 16 | transition: background 0.3s; 17 | 18 | @media (min-width: 340px) { 19 | padding: 9px 16px; 20 | } 21 | 22 | @media (min-width: 460px) { 23 | padding: 9px 12px; 24 | } 25 | 26 | @media (min-width: 475px) { 27 | padding: 9px 16px; 28 | } 29 | 30 | &:disabled { 31 | cursor: default; 32 | } 33 | 34 | &:not(:last-child) { 35 | margin-right: 8px; 36 | 37 | @media (min-width: 360px) { 38 | margin-right: 12px; 39 | } 40 | } 41 | 42 | &:not(:disabled) { 43 | &:hover, 44 | &:focus { 45 | background: #5a2aff; 46 | } 47 | } 48 | } 49 | 50 | .icon { 51 | height: 100%; 52 | } 53 | 54 | .text { 55 | display: none; 56 | margin-left: 10px; 57 | font-weight: 900; 58 | 59 | @media (min-width: 460px) { 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /components/Modal/RemoveDeck/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | 3 | import { ModalShowingProps } from '..' 4 | import Deck from 'models/Deck' 5 | import useCurrentUser from 'hooks/useCurrentUser' 6 | import ConfirmationModal from '../Confirmation' 7 | 8 | export interface RemoveDeckModalProps extends ModalShowingProps { 9 | deck: Deck | null 10 | } 11 | 12 | const RemoveDeckModal = ({ 13 | deck, 14 | isShowing, 15 | setIsShowing 16 | }: RemoveDeckModalProps) => { 17 | const [currentUser] = useCurrentUser() 18 | 19 | const onConfirm = useCallback(() => { 20 | if (!(deck && currentUser)) return 21 | 22 | deck.remove(currentUser.id) 23 | setIsShowing(false) 24 | }, [deck, currentUser, setIsShowing]) 25 | 26 | return ( 27 | 36 | ) 37 | } 38 | 39 | export default RemoveDeckModal 40 | -------------------------------------------------------------------------------- /components/Modal/RenameSection/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react' 2 | 3 | import { ModalShowingProps } from '..' 4 | import Deck from 'models/Deck' 5 | import Section from 'models/Section' 6 | import InputModal from '../Input' 7 | import handleError from 'lib/handleError' 8 | 9 | export interface RenameSectionModalProps extends ModalShowingProps { 10 | deck: Deck 11 | section: Section | null 12 | } 13 | 14 | const RenameSectionModal = ({ 15 | deck, 16 | section, 17 | isShowing, 18 | setIsShowing 19 | }: RenameSectionModalProps) => { 20 | const [name, setName] = useState('') 21 | 22 | useEffect(() => { 23 | if (isShowing && section) setName(section.name) 24 | }, [isShowing, section, setName]) 25 | 26 | const rename = useCallback(() => { 27 | if (!section) return 28 | 29 | section.rename(deck, name).catch(handleError) 30 | 31 | setIsShowing(false) 32 | }, [section, deck, name, setIsShowing]) 33 | 34 | return ( 35 | 46 | ) 47 | } 48 | 49 | export default RenameSectionModal 50 | -------------------------------------------------------------------------------- /components/Modal/ShareDeck/index.tsx: -------------------------------------------------------------------------------- 1 | import { faLink } from '@fortawesome/free-solid-svg-icons' 2 | 3 | import { ModalShowingProps } from '..' 4 | import Deck from 'models/Deck' 5 | import useCurrentUser from 'hooks/useCurrentUser' 6 | import CopyModal from '../Copy' 7 | 8 | export interface ShareDeckModalProps extends ModalShowingProps { 9 | deck: Deck 10 | } 11 | 12 | const ShareDeckModal = ({ 13 | deck, 14 | isShowing, 15 | setIsShowing 16 | }: ShareDeckModalProps) => { 17 | const [currentUser] = useCurrentUser() 18 | 19 | return ( 20 | 31 | ) 32 | } 33 | 34 | export default ShareDeckModal 35 | -------------------------------------------------------------------------------- /components/Modal/ShareSection/index.tsx: -------------------------------------------------------------------------------- 1 | import { faLink } from '@fortawesome/free-solid-svg-icons' 2 | 3 | import { ModalShowingProps } from '..' 4 | import Deck from 'models/Deck' 5 | import Section from 'models/Section' 6 | import CopyModal from '../Copy' 7 | 8 | export interface ShareSectionModalProps extends ModalShowingProps { 9 | deck: Deck 10 | section: Section | null 11 | } 12 | 13 | const ShareSectionModal = ({ 14 | deck, 15 | section, 16 | isShowing, 17 | setIsShowing 18 | }: ShareSectionModalProps) => ( 19 | 23 | This link unlocks {section?.name ?? '...'} when visited. 24 | 25 | } 26 | icon={faLink} 27 | text={`${deck.urlWithOrigin}/u/${section?.id ?? 'error'}`} 28 | isShowing={isShowing} 29 | setIsShowing={setIsShowing} 30 | /> 31 | ) 32 | 33 | export default ShareSectionModal 34 | -------------------------------------------------------------------------------- /components/Modal/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/z-index'; 2 | 3 | .root { 4 | display: flex; 5 | position: fixed; 6 | justify-content: center; 7 | align-items: center; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | pointer-events: none; 13 | z-index: z-index.$modal; 14 | will-change: background; 15 | transition: background 0.3s; 16 | 17 | &[aria-hidden='false'] { 18 | pointer-events: all; 19 | background: transparentize(black, 0.5); 20 | 21 | .content { 22 | opacity: 1; 23 | transform: none; 24 | } 25 | } 26 | } 27 | 28 | .content { 29 | opacity: 0; 30 | transform: translateY(-100px); 31 | will-change: opacity, transform; 32 | transition: opacity 0.3s, transform 0.3s; 33 | } 34 | -------------------------------------------------------------------------------- /components/NotFound/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | min-height: 100vh; 5 | background: colors.$light-gray; 6 | } 7 | 8 | .title { 9 | margin: 100px 30px 0 30px; 10 | text-align: center; 11 | font-size: 26px; 12 | font-weight: 900; 13 | color: white; 14 | 15 | @media (min-width: 360px) { 16 | margin-top: 120px; 17 | font-size: 30px; 18 | } 19 | 20 | @media (min-width: 470px) { 21 | margin-top: 140px; 22 | font-size: 40px; 23 | } 24 | 25 | @media (min-width: 740px) { 26 | font-size: 60px; 27 | } 28 | 29 | @media (min-width: 1300px) { 30 | margin-top: 180px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/NotFound/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | 3 | import Head, { APP_SCHEMA } from 'components/Head' 4 | import TopGradient from 'components/TopGradient' 5 | import Navbar from 'components/Navbar' 6 | 7 | import styles from './index.module.scss' 8 | 9 | const NotFound: NextPage = () => ( 10 |
11 | [[{ name: '404', url }]]} 15 | schema={[APP_SCHEMA]} 16 | /> 17 | 18 | 19 |

Oh no! Are you lost?

20 |
21 |
22 | ) 23 | 24 | export default NotFound 25 | -------------------------------------------------------------------------------- /components/Notification/index.module.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | font-weight: 900; 3 | } 4 | 5 | .body { 6 | margin: 2px 0 4px; 7 | font-size: 14px; 8 | opacity: 0.7; 9 | } 10 | -------------------------------------------------------------------------------- /components/Notification/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.scss' 2 | 3 | export interface NotificationProps { 4 | title: string 5 | body: string 6 | } 7 | 8 | const Notification = ({ title, body }: NotificationProps) => ( 9 | <> 10 |

{title}

11 |

{body}

12 | 13 | ) 14 | 15 | export default Notification 16 | -------------------------------------------------------------------------------- /components/Notifications/Option/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | display: grid; 5 | grid: 6 | 'input label' auto 7 | '_ data' auto / 8 | auto 1fr; 9 | 10 | & + & { 11 | margin-top: 12px; 12 | } 13 | } 14 | 15 | .input { 16 | grid-area: input; 17 | height: max-content; 18 | margin: 6px 8px 0 0; 19 | } 20 | 21 | .label { 22 | grid-area: label; 23 | color: colors.$dark-gray; 24 | } 25 | 26 | .name, 27 | .info { 28 | display: block; 29 | max-width: 480px; 30 | } 31 | 32 | .name { 33 | font-weight: 900; 34 | } 35 | 36 | .info { 37 | font-size: 14px; 38 | } 39 | 40 | .data { 41 | grid-area: data; 42 | margin-top: 8px; 43 | transition: opacity 0.3s; 44 | 45 | &[aria-disabled='true'] { 46 | pointer-events: none; 47 | opacity: 0.5; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /components/Policy/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | padding: 30px; 5 | color: colors.$dark-gray; 6 | } 7 | 8 | .title { 9 | text-align: center; 10 | } 11 | 12 | .divider { 13 | margin: 30px 0; 14 | background: transparentize(colors.$dark-gray, 0.8); 15 | } 16 | -------------------------------------------------------------------------------- /components/Policy/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | import Head from '../Head' 4 | 5 | import styles from './index.module.scss' 6 | 7 | export interface PolicyProps { 8 | url?: string 9 | description: string 10 | title: string 11 | children?: ReactNode 12 | } 13 | 14 | const Policy = ({ url, title, description, children }: PolicyProps) => ( 15 |
16 | [[{ name: title, url }]]} 21 | schema={[ 22 | { 23 | '@type': 'Article', 24 | headline: title, 25 | name: title, 26 | description 27 | } 28 | ]} 29 | /> 30 |

{title}

31 |
32 | {children} 33 |
34 | ) 35 | 36 | export default Policy 37 | -------------------------------------------------------------------------------- /components/Progress/constants.ts: -------------------------------------------------------------------------------- 1 | export const START_POSITION = 0.3 2 | export const DELAY = 200 3 | -------------------------------------------------------------------------------- /components/Progress/index.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/z-index'; 2 | 3 | $height: 3px; 4 | $color: white; 5 | 6 | #nprogress { 7 | position: relative; 8 | pointer-events: none; 9 | z-index: z-index.$progress; 10 | 11 | .bar { 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | width: 100%; 16 | height: $height; 17 | background: white; 18 | } 19 | 20 | .peg { 21 | display: block; 22 | position: absolute; 23 | right: 0px; 24 | width: 100px; 25 | height: 100%; 26 | box-shadow: 0 0 10px $color, 0 0 5px $color; 27 | opacity: 1; 28 | transform: rotate(3deg) translate(0px, -4px); 29 | } 30 | 31 | .spinner { 32 | display: block; 33 | position: fixed; 34 | top: 15px; 35 | right: 15px; 36 | } 37 | 38 | .spinner-icon { 39 | width: 18px; 40 | height: 18px; 41 | border: solid 2px transparent; 42 | border-top-color: $color; 43 | border-left-color: $color; 44 | border-radius: 50%; 45 | animation: nprogress-spinner 400ms linear infinite; 46 | 47 | @keyframes nprogress-spinner { 48 | to { 49 | transform: rotate(1turn); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /components/ReportMessage/data.ts: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | 3 | import { ReportMessageQuery, ReportMessageProps } from './models' 4 | import users from 'lib/cache/users' 5 | import messages from 'lib/cache/messages' 6 | 7 | export const getServerSideProps: GetServerSideProps< 8 | ReportMessageProps, 9 | ReportMessageQuery 10 | > = async ({ params }) => { 11 | if (!params) return { notFound: true } 12 | const { fromId, toId, messageId } = params 13 | 14 | if (fromId === toId) 15 | return { 16 | redirect: { permanent: true, destination: '/' } 17 | } 18 | 19 | const [from, to, message] = await Promise.all([ 20 | users.get(fromId), 21 | users.get(toId), 22 | messages.get(messageId) 23 | ]) 24 | 25 | if (!(from && to && message)) return { notFound: true } 26 | 27 | if (!(message.from === from.id && message.to === to.id)) 28 | return { 29 | redirect: { permanent: true, destination: '/' } 30 | } 31 | 32 | return { 33 | props: { from } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/ReportMessage/index.module.scss: -------------------------------------------------------------------------------- 1 | .reason { 2 | margin-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /components/ReportMessage/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | import UserData from 'models/User/Data' 4 | 5 | export interface ReportMessageQuery extends ParsedUrlQuery { 6 | fromId: string 7 | toId: string 8 | messageId: string 9 | } 10 | 11 | export interface ReportMessageProps { 12 | from: UserData 13 | } 14 | -------------------------------------------------------------------------------- /components/RestrictContact/data.ts: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | 3 | import { RestrictContactQuery } from './models' 4 | import users from 'lib/cache/users' 5 | 6 | export const getServerSideProps: GetServerSideProps< 7 | Record, 8 | RestrictContactQuery 9 | > = async ({ params }) => { 10 | if (!params) return { notFound: true } 11 | 12 | const user = await users.get(params.id) 13 | if (!user) return { notFound: true } 14 | 15 | return { props: {} } 16 | } 17 | -------------------------------------------------------------------------------- /components/RestrictContact/models.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from 'querystring' 2 | 3 | export interface RestrictContactQuery extends ParsedUrlQuery { 4 | id: string 5 | } 6 | -------------------------------------------------------------------------------- /components/Root/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import dynamic from 'next/dynamic' 3 | 4 | import expectsSignIn from 'lib/expectsSignIn' 5 | import useLayoutAuthState from 'hooks/useLayoutAuthState' 6 | 7 | const Dashboard = dynamic(() => import('components/Dashboard/Home')) 8 | const Landing = dynamic(() => import('components/Home')) 9 | 10 | interface RootProps { 11 | auth: boolean 12 | } 13 | 14 | const Root: NextPage = ({ auth: initialAuthState }) => 15 | useLayoutAuthState() ?? initialAuthState ? ( 16 | 17 | ) : ( 18 | 19 | ) 20 | 21 | Root.getInitialProps = async context => ({ 22 | auth: expectsSignIn(context) ?? false 23 | }) 24 | 25 | export default Root 26 | -------------------------------------------------------------------------------- /components/SectionHeader/ToggleExpandedButton/index.module.scss: -------------------------------------------------------------------------------- 1 | $dimension: 30px; 2 | 3 | .root { 4 | flex-shrink: 0; 5 | width: $dimension; 6 | height: $dimension; 7 | border: 1.5px solid #ddd; 8 | border-radius: 50%; 9 | transition: opacity 0.3s, transform 0.3s; 10 | 11 | &:hover { 12 | opacity: 0.5; 13 | } 14 | } 15 | 16 | .icon { 17 | color: #582efe; 18 | } 19 | -------------------------------------------------------------------------------- /components/SectionHeader/ToggleExpandedButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 2 | import { faPlus, faMinus } from '@fortawesome/free-solid-svg-icons' 3 | 4 | import styles from './index.module.scss' 5 | 6 | export interface SectionHeaderToggleExpandedButtonProps { 7 | degrees: number 8 | toggle?(): void 9 | children: boolean 10 | } 11 | 12 | const SectionHeaderToggleExpandedButton = ({ 13 | degrees, 14 | toggle, 15 | children: isExpanded 16 | }: SectionHeaderToggleExpandedButtonProps) => ( 17 | 27 | ) 28 | 29 | export default SectionHeaderToggleExpandedButton 30 | -------------------------------------------------------------------------------- /components/SectionHeader/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | $margin: 12px; 4 | 5 | .root { 6 | display: flex; 7 | align-items: center; 8 | overflow-x: auto; 9 | cursor: pointer; 10 | font-size: 18px; 11 | 12 | @media (min-width: 800px) { 13 | overflow-x: unset; 14 | } 15 | } 16 | 17 | .name { 18 | font-weight: bold; 19 | color: colors.$dark-gray; 20 | } 21 | 22 | .divider, 23 | .cards, 24 | .share { 25 | flex-shrink: 0; 26 | } 27 | 28 | .divider { 29 | $height: 2px; 30 | 31 | flex-grow: 1; 32 | min-width: 30px; 33 | height: $height; 34 | margin: 0 12px; 35 | background: #ddd; 36 | border-radius: $height / 2; 37 | } 38 | 39 | .cards { 40 | display: none; 41 | margin-right: 12px; 42 | font-weight: bold; 43 | color: #582efe; 44 | transform: translateY(-2px); 45 | 46 | @media (min-width: 470px) { 47 | display: block; 48 | } 49 | } 50 | 51 | .share { 52 | margin: 0 4px 0 14px; 53 | transform: scale(1.3); 54 | 55 | &:hover .shareIcon { 56 | opacity: 0.5; 57 | } 58 | } 59 | 60 | .shareIcon { 61 | fill: #4355f9; 62 | transition: opacity 0.3s; 63 | } 64 | -------------------------------------------------------------------------------- /components/Stars/Star/index.module.scss: -------------------------------------------------------------------------------- 1 | $aspect-ratio: 26.2px / 25px; 2 | $default-height: 25px; 3 | 4 | .root { 5 | flex-shrink: 0; 6 | display: grid; 7 | width: calc(#{$aspect-ratio} * var(--star-height, #{$default-height})); 8 | height: var(--star-height, $default-height); 9 | 10 | &:not(:last-child) { 11 | margin-right: 2px; 12 | } 13 | } 14 | 15 | .background, 16 | .root > picture { 17 | grid-row: 1; 18 | grid-column: 1; 19 | } 20 | 21 | .background { 22 | background: #00d388; 23 | } 24 | -------------------------------------------------------------------------------- /components/Stars/Star/index.tsx: -------------------------------------------------------------------------------- 1 | import Img from 'react-optimized-image' 2 | 3 | import star from 'images/icons/star.jpg' 4 | import styles from './index.module.scss' 5 | 6 | export interface StarProps { 7 | fill: number 8 | } 9 | 10 | const Star = ({ fill }: StarProps) => ( 11 |
12 |
13 | {`Star 14 |
15 | ) 16 | 17 | export default Star 18 | -------------------------------------------------------------------------------- /components/Stars/index.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | } 5 | -------------------------------------------------------------------------------- /components/Stars/index.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames' 2 | 3 | import Star from './Star' 4 | 5 | import styles from './index.module.scss' 6 | 7 | const STARS = [0, 1, 2, 3, 4] as const 8 | 9 | export interface StarsProps { 10 | className?: string 11 | children: number 12 | } 13 | 14 | const Stars = ({ className, children: rating }: StarsProps) => ( 15 |
16 | {STARS.map(offset => ( 17 | 21 | ))} 22 |
23 | ) 24 | 25 | export default Stars 26 | -------------------------------------------------------------------------------- /components/Support/index.tsx: -------------------------------------------------------------------------------- 1 | import Policy from 'components/Policy' 2 | 3 | const Support = () => ( 4 | 8 | Email us at support@memorize.ai or by post to: 9 |
10 |
11 | memorize.ai 12 |
13 | 1717 Curtis Avenue 14 |
15 | Manhattan Beach, CA 90266 16 |
17 | United States 18 |
19 | ) 20 | 21 | export default Support 22 | -------------------------------------------------------------------------------- /components/TextArea/index.module.scss: -------------------------------------------------------------------------------- 1 | @use 'styles/colors'; 2 | 3 | .root { 4 | width: 100%; 5 | padding: 0.25rem 0.5rem; 6 | color: colors.$dark-gray; 7 | border: 2px solid colors.$gray-300; 8 | border-radius: 0.25rem; 9 | outline: none; 10 | 11 | &:hover { 12 | border-color: colors.$gray-400; 13 | } 14 | 15 | &:focus { 16 | border-color: colors.$blue-400; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /components/TextArea/index.tsx: -------------------------------------------------------------------------------- 1 | import { TextareaHTMLAttributes } from 'react' 2 | import cx from 'classnames' 3 | 4 | import styles from './index.module.scss' 5 | 6 | export interface TextAreaProps 7 | extends TextareaHTMLAttributes { 8 | className?: string 9 | minHeight?: string | number 10 | placeholder?: string 11 | value: string 12 | setValue: (value: string) => void 13 | } 14 | 15 | const TextArea = ({ 16 | className, 17 | minHeight, 18 | placeholder, 19 | value, 20 | setValue, 21 | ...props 22 | }: TextAreaProps) => ( 23 |