├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── commitlint.config.js ├── docker-compose.yml ├── fly.toml ├── jest.config.mjs ├── jest.setup.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prisma ├── migrations │ ├── 20230220165457_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── fonts │ ├── Chirp-Bold.woff2 │ ├── Chirp-Heavy.woff2 │ ├── Chirp-Light.woff2 │ ├── Chirp-Medium.woff2 │ └── Chirp-Regular.woff2 ├── media-placeholder.png ├── no-bookmarks.png ├── no-followers.png ├── no-results.png ├── twitter-avatar.jpg ├── twitter-logo.svg └── user_placeholder.png ├── reset.d.ts ├── src ├── app │ ├── [user] │ │ ├── followers │ │ │ └── page.tsx │ │ ├── following │ │ │ └── page.tsx │ │ ├── likes │ │ │ └── page.tsx │ │ ├── media │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── with-replies │ │ │ └── page.tsx │ ├── admin │ │ ├── admin-navbar.tsx │ │ ├── layout.tsx │ │ ├── logs │ │ │ ├── log.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── blocked │ │ │ └── route.ts │ │ ├── bookmarks │ │ │ └── route.ts │ │ ├── error │ │ │ └── route.ts │ │ ├── hashtags │ │ │ └── route.ts │ │ ├── media │ │ │ └── route.ts │ │ ├── messages │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── conversations │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── search │ │ │ ├── people │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── tweets │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ ├── likes │ │ │ │ └── route.ts │ │ │ ├── pin │ │ │ │ └── route.ts │ │ │ ├── quotes │ │ │ │ └── route.ts │ │ │ ├── retweets │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── statistics │ │ │ │ └── route.ts │ │ └── users │ │ │ ├── [id] │ │ │ └── route.ts │ │ │ ├── follow │ │ │ └── route.ts │ │ │ └── route.ts │ ├── bookmarks │ │ └── page.tsx │ ├── explore │ │ └── page.tsx │ ├── global-error.tsx │ ├── hamburger.tsx │ ├── home │ │ ├── client.tsx │ │ ├── page.tsx │ │ └── styles │ │ │ └── home.module.scss │ ├── join-twitter.tsx │ ├── layout.tsx │ ├── messages │ │ ├── [id] │ │ │ ├── client.tsx │ │ │ ├── info │ │ │ │ ├── client.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── client.tsx │ │ ├── page.tsx │ │ └── styles │ │ │ └── messages.module.scss │ ├── not-found.tsx │ ├── notifications │ │ └── page.tsx │ ├── page-fallback.tsx │ ├── page.tsx │ ├── people │ │ ├── client.tsx │ │ └── page.tsx │ ├── search │ │ └── page.tsx │ ├── settings │ │ ├── page.tsx │ │ └── styles │ │ │ └── settings.module.scss │ ├── status │ │ └── [id] │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ └── quotes │ │ │ └── page.tsx │ ├── styles │ │ ├── layout.scss │ │ ├── not-found.module.scss │ │ ├── tailwind.css │ │ └── toast.module.scss │ └── trends │ │ ├── client.tsx │ │ └── page.tsx ├── assets │ ├── back-arrow-icon.tsx │ ├── close-icon.tsx │ ├── comment-icon.tsx │ ├── dot-icon.tsx │ ├── emoji-icon.tsx │ ├── follow-icon.tsx │ ├── gear-icon.tsx │ ├── gif-icon.tsx │ ├── heart-icon.tsx │ ├── image-icon.tsx │ ├── left-arrow-icon.tsx │ ├── location-icon.tsx │ ├── message-icon.tsx │ ├── new-message-icon.tsx │ ├── notifications-icon.tsx │ ├── pin-icon.tsx │ ├── report-icon.tsx │ ├── retweet-icon.tsx │ ├── sad-face-icon.tsx │ ├── search-icon.tsx │ ├── star-icon.tsx │ ├── tick-svg.tsx │ ├── trash-icon.tsx │ ├── twitter-logo.tsx │ ├── user_placeholder.png │ └── verified-icon.tsx ├── components │ └── elements │ │ ├── anchor │ │ ├── components │ │ │ └── anchor.tsx │ │ └── index.ts │ │ ├── button │ │ ├── components │ │ │ ├── button.tsx │ │ │ └── styles │ │ │ │ └── button.module.scss │ │ └── index.ts │ │ ├── create-date │ │ ├── components │ │ │ └── create-date.tsx │ │ └── index.ts │ │ ├── ellipsis-wrapper │ │ ├── components │ │ │ └── ellipsis-wrapper.tsx │ │ └── index.ts │ │ ├── error-fallback │ │ ├── assets │ │ │ └── offline-icon.tsx │ │ ├── error-fallback.tsx │ │ └── index.ts │ │ ├── follow-button │ │ ├── follow-button.tsx │ │ ├── index.ts │ │ ├── styles │ │ │ └── follow-button.module.scss │ │ └── unfollow-modal.tsx │ │ ├── hamburger-button │ │ ├── hamburger-button.tsx │ │ ├── index.ts │ │ └── styles │ │ │ └── hamburger-button.module.scss │ │ ├── loading-screen │ │ ├── index.ts │ │ └── loading-screen.tsx │ │ ├── loading-spinner │ │ ├── assets │ │ │ └── loading-spinner-icon.tsx │ │ ├── index.ts │ │ └── loading-spinner.tsx │ │ ├── menu │ │ ├── components │ │ │ ├── menu-item.tsx │ │ │ └── menu.tsx │ │ └── index.ts │ │ ├── modal │ │ ├── components │ │ │ ├── confirmation-modal.tsx │ │ │ ├── modal.tsx │ │ │ └── styles │ │ │ │ ├── confirmation-modal.module.scss │ │ │ │ └── modal.module.scss │ │ ├── hooks │ │ │ └── use-track-position.ts │ │ └── index.ts │ │ ├── not-found │ │ ├── index.ts │ │ ├── not-found.tsx │ │ └── styles │ │ │ └── not-found.module.scss │ │ ├── progress-bar │ │ ├── index.ts │ │ ├── progress-bar.tsx │ │ └── styles │ │ │ └── progressbar.module.scss │ │ ├── sort-tweets │ │ ├── index.ts │ │ └── sort-tweets.tsx │ │ ├── text-input │ │ ├── components │ │ │ ├── styles │ │ │ │ └── text-input.module.scss │ │ │ └── text-input.tsx │ │ └── index.ts │ │ ├── tooltip │ │ ├── components │ │ │ └── tooltip.tsx │ │ └── index.ts │ │ ├── try-again │ │ ├── assets │ │ │ └── reload-icon.tsx │ │ ├── index.ts │ │ ├── styles │ │ │ └── try-again.module.scss │ │ └── try-again.tsx │ │ └── user-not-found │ │ ├── index.ts │ │ ├── styles │ │ └── user-not-found.module.scss │ │ └── user-not-found.tsx ├── config │ └── index.ts ├── features │ ├── aside │ │ ├── components │ │ │ ├── aside-fallback.tsx │ │ │ ├── aside.tsx │ │ │ └── styles │ │ │ │ └── aside.module.scss │ │ └── index.ts │ ├── auth │ │ ├── api │ │ │ ├── login.ts │ │ │ └── logout.ts │ │ ├── assets │ │ │ ├── apple-logo.tsx │ │ │ └── google-logo.tsx │ │ ├── components │ │ │ ├── AuthButton.tsx │ │ │ ├── auth-flow.tsx │ │ │ ├── change-username.tsx │ │ │ ├── join-twitter-modal.tsx │ │ │ ├── register-form.tsx │ │ │ ├── session-owner-button.tsx │ │ │ ├── session-owner-modal.tsx │ │ │ ├── sign-in-modal.tsx │ │ │ ├── signout-modal.tsx │ │ │ └── styles │ │ │ │ ├── auth-button.module.scss │ │ │ │ ├── auth-modal-trigger.module.scss │ │ │ │ ├── change-username.module.scss │ │ │ │ ├── login-form.module.scss │ │ │ │ ├── register-form.module.scss │ │ │ │ └── session-owner-modal.module.scss │ │ ├── hooks │ │ │ ├── use-auth-flow.ts │ │ │ └── use-join-twitter.ts │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── bookmarks │ │ ├── api │ │ │ ├── add-to-bookmarks.ts │ │ │ ├── delete-all-bookmarks.ts │ │ │ ├── get-bookmarks.ts │ │ │ └── remove-from-bookmarks.ts │ │ ├── components │ │ │ ├── bookmarks-header.tsx │ │ │ ├── bookmarks.tsx │ │ │ ├── no-bookmarks.tsx │ │ │ └── styles │ │ │ │ ├── bookmarks.module.scss │ │ │ │ └── no-bookmarks.module.scss │ │ ├── hooks │ │ │ ├── use-delete-all-bookmarks.ts │ │ │ ├── use-get-bookmarks.ts │ │ │ └── use-toggle-bookmark.ts │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── color-picker │ │ ├── __tests__ │ │ │ └── color-picker.test.tsx │ │ ├── components │ │ │ ├── color-picker.tsx │ │ │ └── color.tsx │ │ └── index.ts │ ├── connect │ │ ├── components │ │ │ ├── connect-fallback.tsx │ │ │ ├── connect-header.tsx │ │ │ ├── connect.tsx │ │ │ ├── person-details.tsx │ │ │ ├── person.tsx │ │ │ └── styles │ │ │ │ ├── connect.module.scss │ │ │ │ ├── person-details.module.scss │ │ │ │ └── person.module.scss │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── create-tweet │ │ ├── __tests__ │ │ │ └── text-progress-bar.test.tsx │ │ ├── api │ │ │ ├── post-media.ts │ │ │ └── post-tweet.ts │ │ ├── assets │ │ │ ├── pen-icon.tsx │ │ │ ├── poll-icon.tsx │ │ │ └── schedule-icon.tsx │ │ ├── components │ │ │ ├── chosen-images.tsx │ │ │ ├── create-tweet-comment.tsx │ │ │ ├── create-tweet-modal.tsx │ │ │ ├── create-tweet-placeholder.tsx │ │ │ ├── create-tweet-quote.tsx │ │ │ ├── create-tweet-wrapper.tsx │ │ │ ├── create-tweet.tsx │ │ │ ├── emoji-button.tsx │ │ │ ├── emoji-picker-modal.tsx │ │ │ ├── mobile-tweet-button.tsx │ │ │ ├── replying-to.tsx │ │ │ ├── styles │ │ │ │ ├── chosen-images.module.scss │ │ │ │ ├── create-tweet-comment.module.scss │ │ │ │ ├── create-tweet-modal.module.scss │ │ │ │ ├── create-tweet-placeholder.module.scss │ │ │ │ ├── create-tweet-quote.module.scss │ │ │ │ ├── create-tweet-wrapper.module.scss │ │ │ │ ├── create-tweet.module.scss │ │ │ │ ├── emoji-picker-modal.module.scss │ │ │ │ ├── replying-to.module.scss │ │ │ │ └── text-progress-bar.module.scss │ │ │ ├── text-progress-bar.tsx │ │ │ └── tweet-button.tsx │ │ ├── hooks │ │ │ └── use-create-tweet.ts │ │ ├── index.ts │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ │ ├── choose-images.ts │ │ │ └── resize-textarea.ts │ ├── explore │ │ ├── api │ │ │ ├── get-hashtags.ts │ │ │ ├── post-hashtags.ts │ │ │ └── retrieve-hashtags-from-tweet.ts │ │ ├── components │ │ │ ├── explore.tsx │ │ │ └── styles │ │ │ │ └── explore.module.scss │ │ ├── hooks │ │ │ └── use-hashtags.ts │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── font-size-customization │ │ ├── __tests__ │ │ │ └── font-size-customizaton.test.tsx │ │ ├── components │ │ │ ├── font-size-customization.tsx │ │ │ └── font-size-slider.tsx │ │ └── index.ts │ ├── footer │ │ ├── assets │ │ │ └── three-dots-icon.tsx │ │ ├── components │ │ │ ├── footer-link.tsx │ │ │ ├── footer.tsx │ │ │ └── styles │ │ │ │ ├── footer.module.scss │ │ │ │ └── link.module.scss │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── header │ │ ├── components │ │ │ ├── explore-header.tsx │ │ │ ├── header.tsx │ │ │ ├── notifications-header.tsx │ │ │ ├── profile-header.tsx │ │ │ └── tweet-header.tsx │ │ └── index.ts │ ├── messages │ │ ├── api │ │ │ ├── create-conversation.ts │ │ │ ├── deleteConversation.ts │ │ │ ├── get-chat.ts │ │ │ ├── get-conversation.ts │ │ │ ├── get-conversations.ts │ │ │ └── post-image.ts │ │ ├── assets │ │ │ ├── arrow-down-icon.tsx │ │ │ ├── info-icon.tsx │ │ │ ├── send-icon.tsx │ │ │ ├── snooze-notifications-icon.tsx │ │ │ ├── user-icon.tsx │ │ │ └── x-icon.tsx │ │ ├── components │ │ │ ├── chat.tsx │ │ │ ├── conversation-card.tsx │ │ │ ├── conversation-header.tsx │ │ │ ├── conversation-member-details.tsx │ │ │ ├── conversation.tsx │ │ │ ├── conversations.tsx │ │ │ ├── info │ │ │ │ ├── conversation-actions.tsx │ │ │ │ ├── conversation-info-header.tsx │ │ │ │ ├── conversation-info.tsx │ │ │ │ ├── conversation-member.tsx │ │ │ │ ├── conversation-notifications.tsx │ │ │ │ └── styles │ │ │ │ │ ├── conversation-actions.module.scss │ │ │ │ │ ├── conversation-info.module.scss │ │ │ │ │ ├── conversation-member.module.scss │ │ │ │ │ └── conversation-notifications.module.scss │ │ │ ├── message-input.tsx │ │ │ ├── message.tsx │ │ │ ├── new-message │ │ │ │ ├── contact.tsx │ │ │ │ ├── contacts.tsx │ │ │ │ ├── new-message-header.tsx │ │ │ │ ├── new-message-modal.tsx │ │ │ │ ├── search-people-results.tsx │ │ │ │ ├── search-people.tsx │ │ │ │ └── styles │ │ │ │ │ ├── contact.module.scss │ │ │ │ │ ├── new-message-header.module.scss │ │ │ │ │ ├── new-message-modal.module.scss │ │ │ │ │ └── search-people.module.scss │ │ │ ├── search-conversation-results.tsx │ │ │ ├── search-conversations.tsx │ │ │ ├── start-new-conversation.tsx │ │ │ └── styles │ │ │ │ ├── conversations.module.scss │ │ │ │ ├── message-input.module.scss │ │ │ │ ├── message.module.scss │ │ │ │ ├── search-conversations.module.scss │ │ │ │ ├── search-results.module.scss │ │ │ │ └── start-new-conversation.module.scss │ │ ├── hooks │ │ │ ├── use-create-conversation.ts │ │ │ ├── use-delete-conversation.ts │ │ │ ├── use-get-chat.ts │ │ │ ├── use-get-conversation.ts │ │ │ ├── use-get-conversations.ts │ │ │ └── use-socket-events.ts │ │ ├── index.ts │ │ ├── stores │ │ │ └── use-new-message-store.ts │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ │ ├── choose-image.ts │ │ │ ├── create-new-message.ts │ │ │ ├── handle-focus.ts │ │ │ ├── remove-message-from-query.ts │ │ │ ├── resend-message.ts │ │ │ ├── scroll-into-view.ts │ │ │ └── update-query-data.ts │ ├── navbar │ │ ├── assets │ │ │ ├── addition-icon.tsx │ │ │ ├── bell-icon.tsx │ │ │ ├── bookmark-icon.tsx │ │ │ ├── dashboard-icon.tsx │ │ │ ├── envelope-icon.tsx │ │ │ ├── gear-icon.tsx │ │ │ ├── hashtag-icon.tsx │ │ │ ├── home-icon.tsx │ │ │ ├── plus-icon.tsx │ │ │ ├── search-icon.tsx │ │ │ └── user-icon.tsx │ │ ├── components │ │ │ ├── hamburger-menu.tsx │ │ │ ├── mobile-navbar.tsx │ │ │ ├── navbar-item.tsx │ │ │ ├── navbar.tsx │ │ │ └── styles │ │ │ │ └── hamburger-menu.module.scss │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── profile │ │ ├── __tests__ │ │ │ └── follows-navigation.test.tsx │ │ ├── api │ │ │ ├── follow-user.ts │ │ │ ├── get-follows.ts │ │ │ ├── get-pinned-tweet.ts │ │ │ ├── get-user-likes.ts │ │ │ ├── get-user-metadata.ts │ │ │ ├── get-user-tweets-with-media.ts │ │ │ ├── get-user-tweets-with-replies.ts │ │ │ ├── get-user-tweets.ts │ │ │ ├── get-user.ts │ │ │ ├── get-users.ts │ │ │ ├── post-image.ts │ │ │ ├── unfollow-user.ts │ │ │ └── update-profile.ts │ │ ├── assets │ │ │ ├── calendar-icon.tsx │ │ │ ├── camera-icon.tsx │ │ │ └── website-icon.tsx │ │ ├── components │ │ │ ├── avatar.tsx │ │ │ ├── edit-profile-modal.tsx │ │ │ ├── followers.tsx │ │ │ ├── following.tsx │ │ │ ├── follows-link.tsx │ │ │ ├── follows-navigation.tsx │ │ │ ├── inspect-image-modal.tsx │ │ │ ├── link-to-profile.tsx │ │ │ ├── navigation-tab.tsx │ │ │ ├── no-followers.tsx │ │ │ ├── pinned-tweet.tsx │ │ │ ├── profile-info.tsx │ │ │ ├── profile-likes.tsx │ │ │ ├── profile-media.tsx │ │ │ ├── profile-navigation.tsx │ │ │ ├── profile-tweets-and-replies.tsx │ │ │ ├── profile-tweets.tsx │ │ │ ├── profile.tsx │ │ │ ├── styles │ │ │ │ ├── edit-profile-modal.module.scss │ │ │ │ ├── follows-link.module.scss │ │ │ │ ├── follows-navigation.module.scss │ │ │ │ ├── inspect-image-modal.module.scss │ │ │ │ ├── link-to-profile.module.scss │ │ │ │ ├── navigation-tab.module.scss │ │ │ │ ├── no-followers.module.scss │ │ │ │ ├── profile-likes.module.scss │ │ │ │ ├── profile-media.module.scss │ │ │ │ ├── profile-navigation.module.scss │ │ │ │ ├── profile-tweets-and-replies.module.scss │ │ │ │ ├── profile-tweets.module.scss │ │ │ │ ├── profile.module.scss │ │ │ │ ├── user-info.module.scss │ │ │ │ ├── user-join-date.module.scss │ │ │ │ ├── user-modal-wrapper.module.scss │ │ │ │ ├── user-modal.module.scss │ │ │ │ └── user-screen-name.module.scss │ │ │ ├── user-join-date.tsx │ │ │ ├── user-modal-wrapper.tsx │ │ │ ├── user-modal.tsx │ │ │ ├── user-name.tsx │ │ │ └── user-screen-name.tsx │ │ ├── hooks │ │ │ ├── use-focus-on-active-tab.tsx │ │ │ ├── use-follow.ts │ │ │ ├── use-get-follows.ts │ │ │ ├── use-pinned-tweet.ts │ │ │ ├── use-user-likes.ts │ │ │ ├── use-user.ts │ │ │ └── use-users.ts │ │ ├── index.ts │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ │ ├── following.ts │ │ │ └── handle-navigation-interaction.ts │ ├── search │ │ ├── api │ │ │ ├── get-query-people.ts │ │ │ └── get-search-results.ts │ │ ├── assets │ │ │ ├── search-close-icon.tsx │ │ │ └── search-icon.tsx │ │ ├── components │ │ │ ├── no-results.tsx │ │ │ ├── search-header.tsx │ │ │ ├── search-results-modal.tsx │ │ │ ├── search-results.tsx │ │ │ ├── search.tsx │ │ │ └── styles │ │ │ │ ├── no-results.module.scss │ │ │ │ ├── search-header.module.scss │ │ │ │ ├── search-results-modal.module.scss │ │ │ │ ├── search-results.module.scss │ │ │ │ └── search.module.scss │ │ ├── hooks │ │ │ ├── use-search-people.ts │ │ │ └── use-search.ts │ │ ├── index.ts │ │ ├── stores │ │ │ └── use-search.ts │ │ └── types │ │ │ └── index.ts │ ├── sidebar │ │ ├── assets │ │ │ ├── logo-icon.tsx │ │ │ └── options-icon.tsx │ │ ├── components │ │ │ ├── logo.tsx │ │ │ ├── sidebar.tsx │ │ │ └── styles │ │ │ │ ├── logo.module.scss │ │ │ │ └── sidebar.module.scss │ │ └── index.ts │ ├── themes │ │ ├── __tests__ │ │ │ └── theme-picker.test.tsx │ │ ├── components │ │ │ ├── theme-picker.tsx │ │ │ └── theme.tsx │ │ └── index.ts │ ├── trends │ │ ├── components │ │ │ ├── styles │ │ │ │ ├── trend.module.scss │ │ │ │ └── trends.module.scss │ │ │ ├── trend-options.tsx │ │ │ ├── trend.tsx │ │ │ ├── trends-fallback.tsx │ │ │ ├── trends-header.tsx │ │ │ └── trends.tsx │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ └── tweets │ │ ├── api │ │ ├── delete-media.ts │ │ ├── delete-tweet.ts │ │ ├── get-tweet-metadata.ts │ │ ├── get-tweet.ts │ │ ├── get-tweets.ts │ │ ├── handle-retweet.ts │ │ ├── pin-tweet.ts │ │ ├── toggle-like.ts │ │ └── unpin-tweet.ts │ │ ├── assets │ │ ├── backward-arrow-icon.tsx │ │ ├── block-icon.tsx │ │ ├── bookmark-icon.tsx │ │ ├── copy-link-icon.tsx │ │ ├── double-arrows-icon.tsx │ │ ├── edit-icon.tsx │ │ ├── embed-icon.tsx │ │ ├── follow-icon.tsx │ │ ├── forward-arrow-icon.tsx │ │ ├── mute-icon.tsx │ │ ├── pin-icon.tsx │ │ ├── quote-tweet-icon.tsx │ │ └── share-icon.tsx │ │ ├── components │ │ ├── actions │ │ │ ├── comment-button.tsx │ │ │ ├── like-button.tsx │ │ │ ├── retweet-button.tsx │ │ │ └── share-button.tsx │ │ ├── comments.tsx │ │ ├── delete-tweet-modal.tsx │ │ ├── image-carousel.tsx │ │ ├── infinite-tweets.tsx │ │ ├── inspect-tweet-image-modal.tsx │ │ ├── options │ │ │ ├── tweet-options.tsx │ │ │ ├── tweet-owner-menu.tsx │ │ │ └── tweet-visitor-menu.tsx │ │ ├── quoted-tweet.tsx │ │ ├── styles │ │ │ ├── comments.module.scss │ │ │ ├── image-carousel.module.scss │ │ │ ├── infinite-tweets.module.scss │ │ │ ├── inspect-tweet-image-modal.module.scss │ │ │ ├── quoted-tweet.module.scss │ │ │ ├── tweet-actions.module.scss │ │ │ ├── tweet-author.module.scss │ │ │ ├── tweet-details.module.scss │ │ │ ├── tweet-media.module.scss │ │ │ ├── tweet-quotes.module.scss │ │ │ ├── tweet-statistics-modal.module.scss │ │ │ ├── tweet-statistics.module.scss │ │ │ ├── tweet.module.scss │ │ │ └── tweets.module.scss │ │ ├── tweet-actions.tsx │ │ ├── tweet-author.tsx │ │ ├── tweet-creation-date.tsx │ │ ├── tweet-details.tsx │ │ ├── tweet-media.tsx │ │ ├── tweet-quotes.tsx │ │ ├── tweet-statistics-modal.tsx │ │ ├── tweet-statistics.tsx │ │ ├── tweet.tsx │ │ └── tweets.tsx │ │ ├── hooks │ │ ├── use-delete-tweet.ts │ │ ├── use-like.ts │ │ ├── use-pin-tweet.ts │ │ ├── use-retweet.ts │ │ ├── use-tweet.ts │ │ └── use-tweets.ts │ │ ├── index.ts │ │ └── types │ │ └── index.ts ├── hooks │ ├── index.ts │ ├── types │ │ └── index.ts │ ├── use-debounce.ts │ └── use-disable-body-scroll.tsx ├── lib │ ├── prisma.ts │ └── socket-io.ts ├── middleware.ts ├── providers │ ├── error-boundary-provider.tsx │ ├── index.tsx │ ├── next-auth-provider.tsx │ └── react-query-provider.tsx ├── sass │ ├── abstracts │ │ ├── _colors.scss │ │ ├── _index.scss │ │ ├── _media-query.scss │ │ ├── _themes.scss │ │ └── _z-index.scss │ ├── base │ │ ├── _base.scss │ │ ├── _font-size.scss │ │ ├── _index.scss │ │ ├── _reset.scss │ │ └── _typography.scss │ └── main.scss ├── stores │ ├── use-auth-modal.ts │ ├── use-create-tweet-modal.ts │ ├── use-hamburger.ts │ └── use-tweet-statistics.ts ├── types │ └── next-auth.d.ts └── utils │ ├── cn.ts │ └── supabase-client.ts ├── tailwind.config.js └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | ### STANDARD GIT IGNORE FILE ### 2 | 3 | # DEPENDENCIES 4 | node_modules/ 5 | /.pnp 6 | .pnp.js 7 | package-lock.json 8 | yarn.lock 9 | 10 | # TESTING 11 | /coverage 12 | *.lcov 13 | .nyc_output 14 | 15 | # BUILD 16 | build/ 17 | public/build/ 18 | dist/ 19 | generated/ 20 | 21 | # ENV FILES 22 | .env 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | # LOGS 29 | logs 30 | *.log 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # MISC 36 | .idea 37 | .turbo/ 38 | .cache/ 39 | .next/ 40 | .nuxt/ 41 | tmp/ 42 | temp/ 43 | .eslintcache 44 | .docusaurus 45 | 46 | # MAC 47 | ._* 48 | .DS_Store 49 | Thumbs.db 50 | 51 | .turbo 52 | .vercel 53 | 54 | # typescript 55 | *.tsbuildinfo 56 | next-env.d.ts 57 | 58 | Dockerfile 59 | .dockerignore 60 | .git 61 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | ### STANDARD GIT IGNORE FILE ### 2 | 3 | # DEPENDENCIES 4 | node_modules/ 5 | /.pnp 6 | .pnp.js 7 | package-lock.json 8 | yarn.lock 9 | 10 | # TESTING 11 | /coverage 12 | *.lcov 13 | .nyc_output 14 | 15 | # BUILD 16 | build/ 17 | public/build/ 18 | dist/ 19 | generated/ 20 | 21 | # ENV FILES 22 | .env 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | # LOGS 29 | logs 30 | *.log 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # MISC 36 | .idea 37 | .turbo/ 38 | .cache/ 39 | .next/ 40 | .nuxt/ 41 | tmp/ 42 | temp/ 43 | .eslintcache 44 | .docusaurus 45 | 46 | # MAC 47 | ._* 48 | .DS_Store 49 | Thumbs.db 50 | 51 | .turbo 52 | .vercel 53 | *.css 54 | *.scss 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### STANDARD GIT IGNORE FILE ### 2 | 3 | # DEPENDENCIES 4 | node_modules/ 5 | /.pnp 6 | .pnp.js 7 | package-lock.json 8 | yarn.lock 9 | 10 | # TESTING 11 | /coverage 12 | *.lcov 13 | .nyc_output 14 | 15 | # BUILD 16 | build/ 17 | public/build/ 18 | dist/ 19 | generated/ 20 | 21 | # ENV FILES 22 | .env 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | # LOGS 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # MISC 34 | .idea 35 | .turbo/ 36 | .cache/ 37 | .next/ 38 | .nuxt/ 39 | tmp/ 40 | temp/ 41 | .eslintcache 42 | .docusaurus 43 | 44 | # MAC 45 | ._* 46 | .DS_Store 47 | Thumbs.db 48 | 49 | .turbo 50 | .vercel 51 | 52 | # typescript 53 | *.tsbuildinfo 54 | next-env.d.ts 55 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm build 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 3 | "eslint --fix", 4 | "prettier --write" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ### STANDARD GIT IGNORE FILE ### 2 | 3 | # DEPENDENCIES 4 | node_modules/ 5 | /.pnp 6 | .pnp.js 7 | package-lock.json 8 | yarn.lock 9 | 10 | # TESTING 11 | /coverage 12 | *.lcov 13 | .nyc_output 14 | 15 | # BUILD 16 | build/ 17 | public/build/ 18 | dist/ 19 | generated/ 20 | 21 | # ENV FILES 22 | .env 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | # LOGS 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # MISC 34 | .idea 35 | .turbo/ 36 | .cache/ 37 | .next/ 38 | .nuxt/ 39 | tmp/ 40 | temp/ 41 | .eslintcache 42 | .docusaurus 43 | 44 | # MAC 45 | ._* 46 | .DS_Store 47 | Thumbs.db 48 | 49 | .turbo 50 | .vercel 51 | 52 | # typescript 53 | *.tsbuildinfo 54 | next-env.d.ts 55 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"], 3 | "arrowParens": "always", 4 | "printWidth": 80, 5 | "singleQuote": false, 6 | "jsxSingleQuote": false, 7 | "semi": true, 8 | "trailingComma": "all", 9 | "tabWidth": 2 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "cSpell.words": [ 5 | "commitlint", 6 | "DDTHH", 7 | "hookform", 8 | "NEXTAUTH", 9 | "noninteractive", 10 | "paralleldrive", 11 | "progressbar", 12 | "signin", 13 | "spammy", 14 | "supa", 15 | "Toastify", 16 | "typeahead" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | app: 4 | container_name: chirp 5 | build: 6 | context: . 7 | args: 8 | NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} 9 | NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} 10 | NEXT_PUBLIC_SOCKET_URL: ${NEXT_PUBLIC_SOCKET_URL} 11 | image: chirp 12 | ports: 13 | - "3000:3000" 14 | env_file: 15 | - .env 16 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for twitter on 2024-02-16T17:45:29+04:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'twitter' 7 | primary_region = 'ams' 8 | 9 | [build] 10 | 11 | [http_service] 12 | internal_port = 3000 13 | force_https = true 14 | auto_stop_machines = true 15 | auto_start_machines = true 16 | min_machines_running = 0 17 | processes = ['app'] 18 | 19 | [[vm]] 20 | memory = '1gb' 21 | cpu_kind = 'shared' 22 | cpus = 1 23 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import nextJest from "next/jest.js"; 3 | 4 | const createJestConfig = nextJest({ 5 | dir: ".", 6 | }); 7 | 8 | dotenv.config({ path: "./.env.local" }); 9 | 10 | /** @type {import('jest').Config} */ 11 | const config = { 12 | testEnvironment: "jest-environment-jsdom", 13 | setupFilesAfterEnv: ["/jest.setup.js"], 14 | moduleNameMapper: { "^@/(.*)$": "/src/$1" }, 15 | }; 16 | 17 | export default createJestConfig(config); 18 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /** @type {import('next').NextConfig} */ 3 | const path = require("path"); 4 | const nextConfig = { 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: "https", 9 | hostname: "hbrhodokmkprrksqwoph.supabase.co", 10 | }, 11 | ], 12 | }, 13 | 14 | output: process.env.BUILD_STANDALONE === "true" ? "standalone" : undefined, 15 | 16 | sassOptions: { 17 | includePaths: [path.join(__dirname, "src/sass")], 18 | }, 19 | }; 20 | 21 | module.exports = nextConfig; 22 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "tailwindcss/nesting": {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /public/fonts/Chirp-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/fonts/Chirp-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/Chirp-Heavy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/fonts/Chirp-Heavy.woff2 -------------------------------------------------------------------------------- /public/fonts/Chirp-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/fonts/Chirp-Light.woff2 -------------------------------------------------------------------------------- /public/fonts/Chirp-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/fonts/Chirp-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/Chirp-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/fonts/Chirp-Regular.woff2 -------------------------------------------------------------------------------- /public/media-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/media-placeholder.png -------------------------------------------------------------------------------- /public/no-bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/no-bookmarks.png -------------------------------------------------------------------------------- /public/no-followers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/no-followers.png -------------------------------------------------------------------------------- /public/no-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/no-results.png -------------------------------------------------------------------------------- /public/twitter-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/twitter-avatar.jpg -------------------------------------------------------------------------------- /public/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/user_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/public/user_placeholder.png -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | import "@total-typescript/ts-reset"; 2 | -------------------------------------------------------------------------------- /src/app/admin/admin-navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { usePathname } from "next/navigation"; 4 | 5 | import { cn } from "@/utils/cn"; 6 | 7 | export const AdminNavbar = () => { 8 | const pathname = usePathname(); 9 | const path = pathname.split("/").at(-1); 10 | 11 | return ( 12 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/features/header"; 2 | 3 | import { AdminNavbar } from "./admin-navbar"; 4 | 5 | const AdminLayout = ({ children }: { children: React.ReactNode }) => { 6 | return ( 7 |
8 |
9 | 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | 16 | export default AdminLayout; 17 | -------------------------------------------------------------------------------- /src/app/admin/logs/page.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prisma"; 2 | import { Log } from "./log"; 3 | 4 | const getLogs = async () => { 5 | const data = await prisma.errorLog.findMany({}); 6 | return data; 7 | }; 8 | 9 | async function LogsPage() { 10 | const logs = await getLogs(); 11 | 12 | if (!logs || logs.length === 0) { 13 | return ( 14 |
15 |

There are no logs in this time range

16 | 17 |
18 | ); 19 | } 20 | 21 | return ( 22 |
23 | {logs.map((log) => { 24 | return ; 25 | })} 26 |
27 | ); 28 | } 29 | 30 | export default LogsPage; 31 | 32 | export const metadata = { 33 | title: "Logs", 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | const AdminPage = () => { 2 | return
This is admin panel
; 3 | }; 4 | 5 | export default AdminPage; 6 | 7 | export const metadata = { 8 | title: "Admin", 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/api/blocked/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export async function GET() { 4 | return NextResponse.json( 5 | { 6 | message: "You have been rate limited, please try again later.", 7 | }, 8 | { 9 | status: 429, 10 | }, 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/api/tweets/statistics/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prisma"; 2 | 3 | export async function GET(request: Request) { 4 | const { searchParams } = new URL(request.url); 5 | const tweet_id = searchParams.get("tweet_id"); 6 | const type = searchParams.get("type"); 7 | 8 | try { 9 | const authors = await prisma.user.findMany({ 10 | where: { 11 | ...(type === "retweets" && { 12 | retweets: { 13 | some: { 14 | tweet_id: tweet_id as string, 15 | }, 16 | }, 17 | }), 18 | 19 | ...(type === "likes" && { 20 | likes: { 21 | some: { 22 | tweet_id: tweet_id as string, 23 | }, 24 | }, 25 | }), 26 | }, 27 | }); 28 | 29 | return Response.json(authors, { 30 | status: 200, 31 | }); 32 | } catch (error: any) { 33 | return Response.json( 34 | { error: error.message }, 35 | { 36 | status: 500, 37 | }, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/bookmarks/page.tsx: -------------------------------------------------------------------------------- 1 | import { Bookmarks } from "@/features/bookmarks"; 2 | 3 | const BookmarksPage = () => { 4 | return ; 5 | }; 6 | 7 | export default BookmarksPage; 8 | 9 | export const metadata = { 10 | title: "Bookmarks", 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/explore/page.tsx: -------------------------------------------------------------------------------- 1 | import { Explore } from "@/features/explore"; 2 | 3 | const ExplorePage = () => { 4 | return ; 5 | }; 6 | 7 | export default ExplorePage; 8 | 9 | export const metadata = { 10 | title: "Explore", 11 | description: "The latest stories on Chirp - as told by Tweets.", 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ErrorFallback } from "@/components/elements/error-fallback"; 4 | 5 | export default function GlobalError({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error & { digest?: string }; 10 | reset: () => void; 11 | }) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/hamburger.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { AnimatePresence } from "framer-motion"; 3 | 4 | import { Modal } from "@/components/elements/modal"; 5 | import { HamburgerMenu } from "@/features/navbar"; 6 | import { useHamburger } from "@/stores/use-hamburger"; 7 | 8 | export const Hamburger = () => { 9 | const isHamburgerOpen = useHamburger((state) => state.isHamburgerOpen); 10 | const closeHamburger = useHamburger((state) => state.closeHamburger); 11 | 12 | return ( 13 | 14 | {isHamburgerOpen && ( 15 | closeHamburger()} closeOnBackdropClick={true}> 16 | 17 | 18 | )} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/home/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { HamburgerButton } from "@/components/elements/hamburger-button"; 3 | import { SortTweets } from "@/components/elements/sort-tweets"; 4 | import { CreateTweet } from "@/features/create-tweet"; 5 | import { Header } from "@/features/header"; 6 | import { Tweets } from "@/features/tweets"; 7 | 8 | import styles from "./styles/home.module.scss"; 9 | 10 | export const HomeClientPage = () => { 11 | return ( 12 |
13 |
14 | 15 |

Home

16 |
17 | 18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/app/home/page.tsx: -------------------------------------------------------------------------------- 1 | import { HomeClientPage } from "./client"; 2 | 3 | const Home = () => { 4 | return ; 5 | }; 6 | 7 | export default Home; 8 | 9 | export const metadata = { 10 | title: "Home", 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/home/styles/home.module.scss: -------------------------------------------------------------------------------- 1 | @use "./abstracts/media-query" as *; 2 | 3 | .container { 4 | .createTweet { 5 | border-bottom: 1px solid var(--clr-border); 6 | display: none; 7 | 8 | @include mq("small") { 9 | display: block; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/join-twitter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { AnimatePresence } from "framer-motion"; 3 | 4 | import { Modal } from "@/components/elements/modal"; 5 | import { JoinTwitterModal, useJoinTwitter } from "@/features/auth"; 6 | 7 | export const JoinTwitter = () => { 8 | const isJoinTwitterModalOpen = useJoinTwitter( 9 | (state) => state.data.isModalOpen, 10 | ); 11 | const setJoinTwitterData = useJoinTwitter((state) => state.setData); 12 | 13 | return ( 14 |
15 | 16 | {isJoinTwitterModalOpen && ( 17 | { 19 | setJoinTwitterData({ 20 | isModalOpen: false, 21 | action: "", 22 | user: "", 23 | }); 24 | }} 25 | disableScroll={true} 26 | > 27 | 28 | 29 | )} 30 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/messages/[id]/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Conversation } from "@/features/messages"; 3 | 4 | export const ConversationClientPage = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/messages/[id]/info/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ConversationInfo } from "@/features/messages"; 4 | 5 | export const ConversationInfoClientPage = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/messages/[id]/info/page.tsx: -------------------------------------------------------------------------------- 1 | import { ConversationInfoClientPage } from "./client"; 2 | 3 | const ConversationInfoPage = () => { 4 | return ; 5 | }; 6 | 7 | export default ConversationInfoPage; 8 | 9 | export const metadata = { 10 | title: "Conversation info", 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/messages/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ConversationClientPage } from "./client"; 2 | 3 | const ConversationPage = () => { 4 | return ; 5 | }; 6 | 7 | export default ConversationPage; 8 | 9 | export const metadata = { 10 | title: "Conversation", 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/messages/page.tsx: -------------------------------------------------------------------------------- 1 | import { MessagesClientPage } from "./client"; 2 | 3 | const MessagesPage = () => { 4 | return ; 5 | }; 6 | 7 | export default MessagesPage; 8 | export const metadata = { 9 | title: "Messages", 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/messages/styles/messages.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | height: 100dvh; 4 | display: grid; 5 | grid-template-rows: auto 1fr; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import styles from "./styles/not-found.module.scss"; 4 | 5 | export default function NotFound() { 6 | return ( 7 |
8 | 9 | Hmm...this page doesn’t exist. Try searching for something else. 10 | 11 | 12 | Search 13 |
14 | ); 15 | } 16 | 17 | export const metadata = { 18 | title: "Page not found", 19 | }; 20 | -------------------------------------------------------------------------------- /src/app/notifications/page.tsx: -------------------------------------------------------------------------------- 1 | import { NotificationsHeader } from "@/features/header"; 2 | 3 | const Notifications = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default Notifications; 12 | 13 | export const metadata = { 14 | title: "Notifications", 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/page-fallback.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export const PageFallback = () => { 4 | return ( 5 |
6 |

7 | Could not load the page, please try again later 8 |

9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Explore } from "@/features/explore"; 2 | 3 | const DefaultPage = () => { 4 | return ; 5 | }; 6 | 7 | export default DefaultPage; 8 | 9 | export const metadata = { 10 | title: "Chirp", 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/people/page.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectClientPage } from "./client"; 2 | 3 | const PeoplePage = () => { 4 | return ; 5 | }; 6 | 7 | export default PeoplePage; 8 | 9 | export const metadata = { 10 | title: "Connect", 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { SearchResults } from "@/features/search"; 2 | 3 | const SearchPage = () => { 4 | return ; 5 | }; 6 | 7 | export default SearchPage; 8 | 9 | export const metadata = { 10 | title: "Search", 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/status/[id]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSpinner } from "@/components/elements/loading-spinner"; 2 | import { TweetHeader } from "@/features/header"; 3 | 4 | export default function Loading() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/status/[id]/quotes/page.tsx: -------------------------------------------------------------------------------- 1 | import { TweetHeader } from "@/features/header"; 2 | import { TweetQuotes } from "@/features/tweets"; 3 | 4 | const Quotes = () => { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Quotes; 14 | 15 | export const metadata = { 16 | title: "Quotes", 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/styles/not-found.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-inline: auto; 3 | padding: 4em 2em; 4 | display: grid; 5 | place-items: center; 6 | 7 | span { 8 | font-size: var(--fs-milli); 9 | font-weight: var(--fw-400); 10 | color: var(--clr-tertiary); 11 | margin-bottom: 2em; 12 | } 13 | 14 | a { 15 | background-color: var(--clr-primary); 16 | color: var(--clr-secondary); 17 | font-size: var(--fs-milli); 18 | font-weight: var(--fw-700); 19 | padding: 0.6em 1em; 20 | border-radius: 100vmax; 21 | cursor: pointer; 22 | transition: background-color 0.2s ease-in-out; 23 | 24 | &:hover { 25 | background-color: var(--clr-primary-hover); 26 | } 27 | 28 | &:active { 29 | background-color: var(--clr-primary-active); 30 | } 31 | 32 | &:focus-visible { 33 | background-color: var(--clr-primary-hover); 34 | outline: 2px solid var(--clr-primary-disabled); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/styles/toast.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | place-content: center; 4 | 5 | .toast { 6 | background-color: var(--clr-primary); 7 | color: var(--clr-secondary); 8 | box-shadow: none; 9 | font-size: var(--fs-micro); 10 | min-width: fit-content; 11 | min-height: fit-content; 12 | padding: 0.6em 0.8em; 13 | cursor: text; 14 | animation-duration: 0.2s; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/trends/page.tsx: -------------------------------------------------------------------------------- 1 | import { TrendsClientPage } from "./client"; 2 | 3 | const TrendsPage = () => { 4 | return ; 5 | }; 6 | 7 | export default TrendsPage; 8 | 9 | export const metadata = { 10 | title: "Trends", 11 | }; 12 | -------------------------------------------------------------------------------- /src/assets/back-arrow-icon.tsx: -------------------------------------------------------------------------------- 1 | export const BackArrowIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/close-icon.tsx: -------------------------------------------------------------------------------- 1 | export const CloseIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/comment-icon.tsx: -------------------------------------------------------------------------------- 1 | export const CommentIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const CommentIconFill = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/assets/dot-icon.tsx: -------------------------------------------------------------------------------- 1 | export const DotIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/emoji-icon.tsx: -------------------------------------------------------------------------------- 1 | export const EmojiIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/follow-icon.tsx: -------------------------------------------------------------------------------- 1 | export const FollowIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/gif-icon.tsx: -------------------------------------------------------------------------------- 1 | export const GifIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/image-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ImageIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/left-arrow-icon.tsx: -------------------------------------------------------------------------------- 1 | export const LeftArrowIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/location-icon.tsx: -------------------------------------------------------------------------------- 1 | export const LocationIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/message-icon.tsx: -------------------------------------------------------------------------------- 1 | export const MessageIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/new-message-icon.tsx: -------------------------------------------------------------------------------- 1 | export const NewMessageIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/notifications-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ReceiveNotificationsIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const ReceivingNotificationsIcon = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/assets/pin-icon.tsx: -------------------------------------------------------------------------------- 1 | export const PinIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/report-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ReportIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/retweet-icon.tsx: -------------------------------------------------------------------------------- 1 | export const RetweetIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/sad-face-icon.tsx: -------------------------------------------------------------------------------- 1 | export const SadFaceIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/search-icon.tsx: -------------------------------------------------------------------------------- 1 | export const SearchIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/star-icon.tsx: -------------------------------------------------------------------------------- 1 | export const StarIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/tick-svg.tsx: -------------------------------------------------------------------------------- 1 | export const TickIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/trash-icon.tsx: -------------------------------------------------------------------------------- 1 | export const TrashIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/twitter-logo.tsx: -------------------------------------------------------------------------------- 1 | export const TwitterLogo = () => { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/user_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/src/assets/user_placeholder.png -------------------------------------------------------------------------------- /src/assets/verified-icon.tsx: -------------------------------------------------------------------------------- 1 | export const VerifiedIcon = () => { 2 | return ( 3 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/elements/anchor/components/anchor.tsx: -------------------------------------------------------------------------------- 1 | import Link, { LinkProps } from "next/link"; 2 | 3 | import { cn } from "@/utils/cn"; 4 | 5 | interface ILink extends LinkProps { 6 | children: React.ReactNode; 7 | className?: string; 8 | } 9 | 10 | export const Anchor = ({ children, className, ...props }: ILink) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/elements/anchor/index.ts: -------------------------------------------------------------------------------- 1 | export { Anchor } from "./components/anchor"; 2 | -------------------------------------------------------------------------------- /src/components/elements/button/components/button.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | 3 | import { cn } from "@/utils/cn"; 4 | 5 | interface IButton extends React.ButtonHTMLAttributes { 6 | children: React.ReactNode; 7 | } 8 | 9 | export const Button = forwardRef( 10 | ({ children, className, ...props }, ref) => { 11 | return ( 12 | 27 | ); 28 | }, 29 | ); 30 | 31 | Button.displayName = "Button"; 32 | -------------------------------------------------------------------------------- /src/components/elements/button/components/styles/button.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | cursor: pointer; 3 | display: grid; 4 | place-items: center; 5 | padding: 0.5em; 6 | border-radius: 100vmax; 7 | transition: background-color 0.15s ease-in-out; 8 | 9 | svg { 10 | width: var(--fs-h2); 11 | height: var(--fs-h2); 12 | fill: var(--clr-secondary); 13 | } 14 | 15 | &:hover { 16 | background-color: var(--clr-nav-hover); 17 | } 18 | 19 | &:active { 20 | background-color: var(--clr-nav-active); 21 | } 22 | 23 | &:focus-visible { 24 | outline: 2px solid var(--clr-secondary); 25 | background-color: var(--clr-nav-hover); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/elements/button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from "./components/button"; 2 | -------------------------------------------------------------------------------- /src/components/elements/create-date/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/create-date"; 2 | -------------------------------------------------------------------------------- /src/components/elements/ellipsis-wrapper/components/ellipsis-wrapper.tsx: -------------------------------------------------------------------------------- 1 | export const EllipsisWrapper = ({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) => { 6 | return ( 7 |
12 | {children} 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/elements/ellipsis-wrapper/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/ellipsis-wrapper"; 2 | -------------------------------------------------------------------------------- /src/components/elements/error-fallback/assets/offline-icon.tsx: -------------------------------------------------------------------------------- 1 | export const OfflineIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/elements/error-fallback/error-fallback.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC } from "react"; 3 | import { createPortal } from "react-dom"; 4 | 5 | import { Button } from "../button"; 6 | import { ReloadIcon } from "../try-again/assets/reload-icon"; 7 | 8 | export interface IErrorFallback { 9 | error: Error; 10 | resetErrorBoundary: () => void; 11 | } 12 | 13 | export const ErrorFallback: FC = ({ 14 | error, 15 | resetErrorBoundary, 16 | }) => { 17 | return createPortal( 18 |
19 |

{error.message}

20 | 27 |
, 28 | document.body, 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/elements/error-fallback/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorFallback } from "./error-fallback"; 2 | -------------------------------------------------------------------------------- /src/components/elements/follow-button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./follow-button"; 2 | -------------------------------------------------------------------------------- /src/components/elements/hamburger-button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hamburger-button"; 2 | -------------------------------------------------------------------------------- /src/components/elements/loading-screen/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./loading-screen"; 2 | -------------------------------------------------------------------------------- /src/components/elements/loading-screen/loading-screen.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect } from "react"; 3 | 4 | import { TwitterLogo } from "@/assets/twitter-logo"; 5 | 6 | export const LoadingScreen = () => { 7 | useEffect(() => { 8 | document.body.style.overflow = "hidden"; 9 | document.body.style.paddingRight = "11px"; 10 | 11 | return () => { 12 | document.body.style.overflow = ""; 13 | document.body.style.paddingRight = ""; 14 | }; 15 | }, []); 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/elements/loading-spinner/assets/loading-spinner-icon.tsx: -------------------------------------------------------------------------------- 1 | export const LoadingSpinnerIcon = () => { 2 | return ( 3 | 4 | 12 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/elements/loading-spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./loading-spinner"; 2 | -------------------------------------------------------------------------------- /src/components/elements/loading-spinner/loading-spinner.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSpinnerIcon } from "./assets/loading-spinner-icon"; 2 | 3 | export const LoadingSpinner = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/elements/menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/menu"; 2 | export * from "./components/menu-item"; 3 | -------------------------------------------------------------------------------- /src/components/elements/modal/components/styles/modal.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | z-index: var(--z-index-modal-backdrop); 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/elements/modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/modal"; 2 | export * from "./components/confirmation-modal"; 3 | export * from "./hooks/use-track-position"; 4 | -------------------------------------------------------------------------------- /src/components/elements/not-found/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./not-found"; 2 | -------------------------------------------------------------------------------- /src/components/elements/not-found/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | 4 | import styles from "./styles/not-found.module.scss"; 5 | 6 | export const NotFound = () => { 7 | const router = useRouter(); 8 | 9 | return ( 10 |
11 |

Hmm...this page doesn’t exist. Try searching for something else.

12 | 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/elements/not-found/styles/not-found.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 38px 11px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | gap: 2rem; 7 | 8 | p { 9 | font-size: var(--xs-milli); 10 | color: var(--clr-tertiary); 11 | } 12 | 13 | button { 14 | background-color: var(--clr-primary); 15 | color: var(--clr-white); 16 | height: 32px; 17 | padding: 0 15px; 18 | border: none; 19 | border-radius: 100vmax; 20 | font-size: var(--xs-milli); 21 | font-weight: var(--fw-700); 22 | cursor: pointer; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/elements/progress-bar/index.ts: -------------------------------------------------------------------------------- 1 | export { ProgressBar } from "./progress-bar"; 2 | -------------------------------------------------------------------------------- /src/components/elements/progress-bar/progress-bar.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./styles/progressbar.module.scss"; 2 | 3 | export const ProgressBar = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/elements/progress-bar/styles/progressbar.module.scss: -------------------------------------------------------------------------------- 1 | .progressbar { 2 | position: relative; 3 | height: 3px; 4 | background-color: var(--clr-background); 5 | overflow: hidden; 6 | 7 | span { 8 | position: absolute; 9 | display: block; 10 | height: 100%; 11 | width: 80px; 12 | border-radius: 100vmax; 13 | background-color: var(--clr-primary); 14 | animation: progress 1s linear infinite; 15 | } 16 | } 17 | 18 | @keyframes progress { 19 | 0% { 20 | left: 0; 21 | } 22 | 23 | 100% { 24 | left: 100%; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/elements/sort-tweets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./sort-tweets"; 2 | -------------------------------------------------------------------------------- /src/components/elements/text-input/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/text-input"; 2 | -------------------------------------------------------------------------------- /src/components/elements/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { Tooltip } from "./components/tooltip"; 2 | -------------------------------------------------------------------------------- /src/components/elements/try-again/assets/reload-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ReloadIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/elements/try-again/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./try-again"; 2 | -------------------------------------------------------------------------------- /src/components/elements/try-again/try-again.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | 4 | import { ReloadIcon } from "./assets/reload-icon"; 5 | import styles from "./styles/try-again.module.scss"; 6 | 7 | export const TryAgain = () => { 8 | const router = useRouter(); 9 | 10 | return ( 11 |
12 |

Something went wrong. Try reloading.

13 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/elements/user-not-found/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user-not-found"; 2 | -------------------------------------------------------------------------------- /src/components/elements/user-not-found/styles/user-not-found.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 380px; 3 | padding: 38px 19px; 4 | margin: 30px auto; 5 | 6 | h1 { 7 | font-size: var(--fs-h1); 8 | font-weight: var(--fs-700); 9 | margin-bottom: 8px; 10 | font-variant: var(--clr-secondary); 11 | } 12 | 13 | p { 14 | font-size: var(--fs-milli); 15 | font-weight: var(--fw-500); 16 | color: var(--clr-tertiary); 17 | margin-bottom: 28px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/elements/user-not-found/user-not-found.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./styles/user-not-found.module.scss"; 2 | 3 | export const UserNotFound = () => { 4 | return ( 5 |
6 |

This account doesn’t exist

7 |

Try searching for another.

8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/aside/components/aside-fallback.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export const AsideFallback = () => { 4 | return ( 5 |
6 | Oops! Something went wrong. 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/aside/index.ts: -------------------------------------------------------------------------------- 1 | export { Aside } from "./components/aside"; 2 | export { AsideFallback } from "./components/aside-fallback"; 3 | -------------------------------------------------------------------------------- /src/features/auth/api/login.ts: -------------------------------------------------------------------------------- 1 | export const loginWithPassword = async () => { 2 | return null; 3 | }; 4 | -------------------------------------------------------------------------------- /src/features/auth/api/logout.ts: -------------------------------------------------------------------------------- 1 | export const logout = () => async () => { 2 | return null; 3 | }; 4 | -------------------------------------------------------------------------------- /src/features/auth/assets/apple-logo.tsx: -------------------------------------------------------------------------------- 1 | export const AppleLogo = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/auth/components/AuthButton.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./styles/auth-button.module.scss"; 2 | 3 | export const AuthButton = ({ 4 | onClick, 5 | icon, 6 | text, 7 | }: { 8 | onClick?: () => void; 9 | icon?: React.ReactNode; 10 | text: string; 11 | }) => { 12 | return ( 13 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/features/auth/components/signout-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { signOut } from "next-auth/react"; 3 | 4 | import { TwitterLogo } from "@/assets/twitter-logo"; 5 | import { ConfirmationModal } from "@/components/elements/modal"; 6 | 7 | export const SignOutModal = ({ onClose }: { onClose: () => void }) => { 8 | return ( 9 | 14 | signOut({ 15 | callbackUrl: "/", 16 | }) 17 | } 18 | confirmButtonStyle="delete" 19 | cancelButtonText="Cancel" 20 | cancelButtonClick={onClose} 21 | logo={} 22 | /> 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/features/auth/components/styles/auth-button.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | padding: 0.7em; 4 | border-radius: 100vmax; 5 | font-size: var(--fs-milli); 6 | font-weight: var(--fw-700); 7 | color: var(--clr-dark); 8 | background-color: var(--clr-light); 9 | border: 1px solid var(--clr-auth-border); 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | cursor: pointer; 14 | transition: background 0.2s ease-in-out; 15 | 16 | &:hover { 17 | background-color: var(--clr-auth-button-hover); 18 | } 19 | 20 | &:active { 21 | background-color: var(--clr-auth-button-active); 22 | } 23 | 24 | &:focus-visible { 25 | outline: 2px solid var(--clr-light); 26 | background-color: var(--clr-auth-button-hover); 27 | } 28 | 29 | svg { 30 | width: var(--fs-h2); 31 | height: var(--fs-h2); 32 | fill: var(--clr-dark); 33 | margin-right: 0.5em; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/features/auth/components/styles/register-form.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: var(--clr-background); 3 | border: 1px solid var(--clr-border); 4 | border-radius: 1rem; 5 | padding: 1em 0.8em; 6 | 7 | .title { 8 | font-size: var(--fs-h3); 9 | font-weight: var(--fw-700); 10 | margin-bottom: 0.5em; 11 | } 12 | 13 | .description { 14 | font-size: var(--fs-nano); 15 | color: var(--clr-tertiary); 16 | margin-bottom: 0.8em; 17 | } 18 | 19 | .buttons { 20 | display: grid; 21 | padding: 0.8em 0; 22 | gap: 0.8em; 23 | } 24 | 25 | .terms { 26 | font-size: var(--fs-nano); 27 | color: var(--clr-tertiary); 28 | line-height: 1.5; 29 | 30 | a { 31 | color: var(--clr-blue); 32 | cursor: pointer; 33 | 34 | &:hover, 35 | &:focus-visible { 36 | text-decoration: underline; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/features/auth/components/styles/session-owner-modal.module.scss: -------------------------------------------------------------------------------- 1 | @use "abstracts/media-query" as *; 2 | 3 | .container { 4 | width: 300px; 5 | background-color: var(--clr-background); 6 | box-shadow: 0 0rem 10px -2px var(--clr-tertiary); 7 | border-radius: 1rem; 8 | padding: 0.8em 0; 9 | overflow: hidden; 10 | 11 | a { 12 | font-size: var(--fs-milli); 13 | font-weight: var(--fw-700); 14 | display: block; 15 | padding: 0.8em 1em; 16 | cursor: pointer; 17 | transition: background-color 0.1s ease-in-out; 18 | 19 | &:hover { 20 | background-color: var(--clr-trends-hover); 21 | } 22 | 23 | &:active { 24 | background-color: var(--clr-trends-active); 25 | } 26 | 27 | &:focus-visible { 28 | background-color: var(--clr-trends-hover); 29 | outline: 2px solid var(--clr-primary-disabled); 30 | outline-offset: -2px; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/features/auth/hooks/use-auth-flow.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface IAuthFlow { 4 | isLogInModalOpen: boolean; 5 | openLogInModal: () => void; 6 | closeLogInModal: () => void; 7 | isLogOutModalOpen: boolean; 8 | openLogOutModal: () => void; 9 | closeLogOutModal: () => void; 10 | } 11 | 12 | export const useAuthFlow = create((set) => ({ 13 | isLogInModalOpen: false, 14 | openLogInModal: () => set({ isLogInModalOpen: true }), 15 | closeLogInModal: () => set({ isLogInModalOpen: false }), 16 | 17 | isLogOutModalOpen: false, 18 | openLogOutModal: () => set({ isLogOutModalOpen: true }), 19 | closeLogOutModal: () => set({ isLogOutModalOpen: false }), 20 | })); 21 | -------------------------------------------------------------------------------- /src/features/auth/hooks/use-join-twitter.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface IJoinTwitter { 4 | data: { 5 | isModalOpen: boolean; 6 | action: string; 7 | user?: string; 8 | }; 9 | setData: (data: { 10 | isModalOpen: boolean; 11 | action: string; 12 | user?: string; 13 | }) => void; 14 | } 15 | 16 | export const useJoinTwitter = create((set) => ({ 17 | data: { 18 | isModalOpen: false, 19 | action: "comment", 20 | user: "user", 21 | }, 22 | 23 | setData: (data) => set({ data }), 24 | })); 25 | -------------------------------------------------------------------------------- /src/features/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/auth-flow"; 2 | export * from "./components/sign-in-modal"; 3 | export * from "./components/register-form"; 4 | export * from "./components/signout-modal"; 5 | export * from "./components/session-owner-button"; 6 | export * from "./components/session-owner-modal"; 7 | export * from "./components/change-username"; 8 | export * from "./components/join-twitter-modal"; 9 | export * from "./hooks/use-join-twitter"; 10 | -------------------------------------------------------------------------------- /src/features/auth/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IProviders { 2 | [key: string]: { 3 | id: string; 4 | name: string; 5 | type: string; 6 | signinUrl: string; 7 | callbackUrl: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/features/bookmarks/api/add-to-bookmarks.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const AddToBookmarks = async ({ 4 | tweetId, 5 | userId, 6 | }: { 7 | tweetId: string | undefined; 8 | userId: string | undefined; 9 | }) => { 10 | try { 11 | const { data } = await axios.post(`/api/bookmarks`, { 12 | tweet_id: tweetId, 13 | user_id: userId, 14 | }); 15 | return data; 16 | } catch (error: any) { 17 | return error.message; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/bookmarks/api/delete-all-bookmarks.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const DeleteAllBookmarks = async (id: string | undefined) => { 4 | try { 5 | const { data } = await axios.delete(`/api/bookmarks?user_id=${id}`); 6 | return data; 7 | } catch (error: any) { 8 | return error.message; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/bookmarks/api/get-bookmarks.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getBookmarks = async (id: string | undefined) => { 4 | try { 5 | const { data } = await axios.get(`/api/bookmarks?user_id=${id}`); 6 | return data; 7 | } catch (error: any) { 8 | return error.message; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/bookmarks/api/remove-from-bookmarks.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const RemoveFromBookmarks = async (bookmarkId: string | undefined) => { 4 | try { 5 | const { data } = await axios.delete( 6 | `/api/bookmarks?bookmark_id=${bookmarkId}`, 7 | ); 8 | return data; 9 | } catch (error: any) { 10 | return error.message; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/features/bookmarks/components/no-bookmarks.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import styles from "./styles/no-bookmarks.module.scss"; 4 | 5 | export const NoBookmarks = () => { 6 | return ( 7 |
8 |
9 |
10 | No bookmarks 16 |
17 |

Save Tweets for later

18 |

19 | Don’t let the good ones fly away! Bookmark Tweets to easily find them 20 | again in the future. 21 |

22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/features/bookmarks/components/styles/bookmarks.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .tweetContainer { 3 | border-bottom: 1px solid var(--clr-border); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/features/bookmarks/components/styles/no-bookmarks.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | place-content: center; 4 | 5 | .content { 6 | max-width: 380px; 7 | padding: 30px; 8 | 9 | .image { 10 | margin-bottom: 30px; 11 | 12 | img { 13 | width: 100%; 14 | height: 100%; 15 | object-fit: cover; 16 | } 17 | } 18 | 19 | h1 { 20 | font-size: var(--fs-kilo); 21 | font-weight: var(--fw-700); 22 | color: var(--clr-secondary); 23 | margin-bottom: 11px; 24 | } 25 | 26 | p { 27 | font-size: var(--fs-milli); 28 | color: var(--clr-tertiary); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/features/bookmarks/hooks/use-delete-all-bookmarks.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 | 3 | import { DeleteAllBookmarks } from "../api/delete-all-bookmarks"; 4 | 5 | export const useDeleteAllBookmarks = () => { 6 | const queryClient = useQueryClient(); 7 | 8 | return useMutation({ 9 | mutationFn: ({ userId }: { userId: string | undefined }) => { 10 | return DeleteAllBookmarks(userId); 11 | }, 12 | 13 | onSuccess: () => { 14 | queryClient.invalidateQueries({ queryKey: ["bookmarks"] }); 15 | queryClient.invalidateQueries({ queryKey: ["tweets"] }); 16 | }, 17 | onError: () => { 18 | console.log("error"); 19 | }, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/features/bookmarks/hooks/use-get-bookmarks.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { getBookmarks } from "../api/get-bookmarks"; 4 | import { IBookmark } from "../types"; 5 | 6 | export const useGetBookmarks = (id: string | undefined) => { 7 | return useQuery({ 8 | queryKey: ["bookmarks", { userId: id }], 9 | queryFn: async () => { 10 | return getBookmarks(id); 11 | }, 12 | 13 | refetchOnWindowFocus: false, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/bookmarks/hooks/use-toggle-bookmark.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 | 3 | import { AddToBookmarks } from "../api/add-to-bookmarks"; 4 | import { RemoveFromBookmarks } from "../api/remove-from-bookmarks"; 5 | 6 | export const useToggleBookmark = () => { 7 | const queryClient = useQueryClient(); 8 | 9 | return useMutation({ 10 | mutationFn: ({ 11 | tweetId, 12 | userId, 13 | bookmarkId, 14 | action, 15 | }: { 16 | tweetId: string | undefined; 17 | userId: string; 18 | bookmarkId?: string; 19 | action: "add" | "remove"; 20 | }) => { 21 | return action === "add" 22 | ? AddToBookmarks({ tweetId, userId }) 23 | : RemoveFromBookmarks(bookmarkId); 24 | }, 25 | 26 | onSuccess: () => { 27 | queryClient.invalidateQueries({ queryKey: ["bookmarks"] }); 28 | queryClient.invalidateQueries({ queryKey: ["tweets"] }); 29 | }, 30 | onError: () => { 31 | console.log("error"); 32 | }, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/features/bookmarks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/bookmarks"; 2 | export * from "./components/bookmarks-header"; 3 | export * from "./hooks/use-toggle-bookmark"; 4 | export * from "./hooks/use-get-bookmarks"; 5 | -------------------------------------------------------------------------------- /src/features/bookmarks/types/index.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from "@/features/profile"; 2 | import { ITweet } from "@/features/tweets"; 3 | 4 | export interface IBookmark { 5 | id: string; 6 | tweet: ITweet; 7 | tweet_id: string; 8 | user: IUser; 9 | user_id: string; 10 | created_at: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/features/color-picker/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/color-picker"; 2 | -------------------------------------------------------------------------------- /src/features/connect/components/connect-fallback.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { Button } from "@/components/elements/button"; 4 | import { IErrorFallback } from "@/components/elements/error-fallback/error-fallback"; 5 | import { ReloadIcon } from "@/components/elements/try-again/assets/reload-icon"; 6 | 7 | export const ConnectFallback: FC = ({ 8 | error, 9 | resetErrorBoundary, 10 | }) => { 11 | return ( 12 |
13 |

{error.message}

14 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/features/connect/components/connect-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | 4 | import { BackArrowIcon } from "@/assets/back-arrow-icon"; 5 | import { Button } from "@/components/elements/button"; 6 | import { Tooltip } from "@/components/elements/tooltip"; 7 | import { Header } from "@/features/header"; 8 | 9 | export const ConnectHeader = () => { 10 | const router = useRouter(); 11 | 12 | return ( 13 |
14 | 15 | 24 | 25 |

Connect

26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/connect/components/styles/connect.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .loading, 3 | .error { 4 | padding: 4rem 0; 5 | } 6 | 7 | h2 { 8 | color: var(--clr-secondary); 9 | font-size: var(--fs-h2); 10 | font-weight: var(--fw-700); 11 | padding: 0.75rem 1rem; 12 | } 13 | 14 | .showMore { 15 | display: block; 16 | padding: 1em; 17 | color: var(--clr-primary); 18 | cursor: pointer; 19 | font-size: var(--fs-base); 20 | border-radius: 0 0 1rem 1rem; 21 | outline-offset: -2px; 22 | 23 | &:hover { 24 | background-color: var(--clr-connect-hover); 25 | } 26 | 27 | &:active { 28 | background-color: var(--clr-connect-active); 29 | } 30 | 31 | &:focus-visible { 32 | outline: 2px solid var(--clr-primary-disabled); 33 | background-color: var(--clr-connect-hover); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/features/connect/components/styles/person-details.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | display: grid; 4 | grid-template-columns: 46px 1fr; 5 | gap: 12px; 6 | padding: 0.75em 1em; 7 | cursor: pointer; 8 | transition: background-color 0.2s ease-in-out; 9 | 10 | &:hover { 11 | background-color: var(--clr-tweet-hover); 12 | } 13 | 14 | &:active { 15 | background-color: var(--clr-quoted-tweet); 16 | } 17 | 18 | &:focus-visible { 19 | outline: 2px solid var(--clr-primary-disabled); 20 | outline-offset: -2px; 21 | background-color: var(--clr-tweet-hover); 22 | } 23 | 24 | &:focus-within { 25 | background-color: var(--clr-tweet-hover); 26 | } 27 | 28 | .info { 29 | .user_details { 30 | display: flex; 31 | align-items: center; 32 | justify-content: space-between; 33 | } 34 | 35 | .secondary { 36 | padding-top: 4px; 37 | 38 | .description { 39 | font-size: var(--fs-milli); 40 | color: var(--clr-secondary); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/features/connect/components/styles/person.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | cursor: pointer; 4 | 5 | &:focus-within { 6 | background-color: var(--clr-connect-hover); 7 | } 8 | 9 | .person { 10 | width: 100%; 11 | padding: 0.75em 1em; 12 | display: grid; 13 | align-items: center; 14 | grid-template-columns: auto 1fr auto; 15 | gap: 12px; 16 | outline-offset: -2px; 17 | transition: background-color 0.2s ease-in-out; 18 | padding-right: calc(var(--fs-kilo) * 3 + 12px); 19 | 20 | &:hover { 21 | background-color: var(--clr-connect-hover); 22 | } 23 | 24 | &:active { 25 | background-color: var(--clr-connect-active); 26 | } 27 | 28 | &:focus-visible { 29 | outline: 2px solid var(--clr-primary-disabled); 30 | background-color: var(--clr-connect-hover); 31 | } 32 | } 33 | 34 | .follow { 35 | position: absolute; 36 | top: 50%; 37 | transform: translateY(-50%); 38 | right: 1em; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/features/connect/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/connect"; 2 | export * from "./components/connect-fallback"; 3 | export * from "./components/person-details"; 4 | export * from "./components/connect-header"; 5 | -------------------------------------------------------------------------------- /src/features/connect/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IPersonProps { 2 | name: string | undefined; 3 | userId: string | undefined; 4 | username: string | undefined; 5 | image: string | undefined; 6 | } 7 | -------------------------------------------------------------------------------- /src/features/create-tweet/assets/pen-icon.tsx: -------------------------------------------------------------------------------- 1 | export const PenIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/create-tweet/assets/poll-icon.tsx: -------------------------------------------------------------------------------- 1 | export const PollIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/create-tweet/assets/schedule-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ScheduleIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/create-tweet/components/create-tweet-placeholder.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar } from "@/features/profile"; 2 | 3 | import styles from "./styles/create-tweet-placeholder.module.scss"; 4 | 5 | export const CreateTweetPlaceholder = ({ 6 | image, 7 | setIsPlaceholder, 8 | }: { 9 | image: string; 10 | setIsPlaceholder: (value: boolean) => void; 11 | }) => { 12 | return ( 13 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/features/create-tweet/components/replying-to.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import styles from "./styles/replying-to.module.scss"; 4 | 5 | export const ReplyingTo = ({ 6 | screen_name, 7 | id, 8 | }: { 9 | screen_name: string | null; 10 | id?: string | null; 11 | }) => { 12 | return ( 13 |
14 | Replying to{" "} 15 | {id ? ( 16 | e.stopPropagation()} 19 | href={`/${id}`} 20 | > 21 | @{screen_name} 22 | 23 | ) : ( 24 | @{screen_name} 25 | )} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/create-tweet/components/styles/create-tweet-comment.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 15px; 3 | display: grid; 4 | grid-template-columns: auto 1fr; 5 | gap: 11px; 6 | padding-bottom: 3px; 7 | 8 | .avatar { 9 | display: grid; 10 | place-items: center; 11 | grid-template-rows: auto 1fr; 12 | gap: 4px; 13 | 14 | .divider { 15 | width: 2px; 16 | min-height: 2rem; 17 | height: 100%; 18 | background-color: var(--clr-border); 19 | } 20 | } 21 | 22 | .userDetails { 23 | display: grid; 24 | grid-auto-flow: column; 25 | justify-content: start; 26 | gap: 4px; 27 | 28 | .dot { 29 | color: var(--clr-tertiary); 30 | } 31 | } 32 | 33 | .tweet { 34 | margin-top: 0.2rem; 35 | 36 | .text { 37 | white-space: pre-wrap; 38 | } 39 | } 40 | 41 | .replyingTo { 42 | padding: 15px 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/features/create-tweet/components/styles/create-tweet-placeholder.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | display: grid; 4 | grid-auto-flow: column; 5 | grid-template-columns: 38px 1fr auto; 6 | align-items: center; 7 | justify-content: start; 8 | gap: 11px; 9 | padding: 11px 15px; 10 | 11 | .avatar { 12 | cursor: pointer; 13 | transition: opacity 0.2s ease-in-out; 14 | 15 | &:hover { 16 | opacity: 0.8; 17 | } 18 | } 19 | 20 | .text { 21 | font-size: var(--fs-h2); 22 | color: var(--clr-tertiary); 23 | overflow: hidden; 24 | text-overflow: ellipsis; 25 | white-space: nowrap; 26 | cursor: text; 27 | } 28 | 29 | .button { 30 | width: 72px; 31 | height: 34px; 32 | border-radius: 100vmax; 33 | background-color: var(--clr-primary); 34 | color: var(--clr-white); 35 | display: grid; 36 | place-items: center; 37 | font-size: var(--fs-milli); 38 | font-weight: 700; 39 | opacity: 0.5; 40 | cursor: default; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/features/create-tweet/components/styles/create-tweet-wrapper.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | border-bottom: 1px solid var(--clr-border); 4 | padding: 4px 0 11px; 5 | 6 | .createTweet { 7 | .replyingTo { 8 | margin-left: 65px; 9 | } 10 | } 11 | } 12 | 13 | @keyframes slideDown { 14 | 0% { 15 | height: 60px; 16 | } 17 | 18 | 100% { 19 | height: auto; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/features/create-tweet/components/styles/emoji-picker-modal.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: min(320px, 100%); 3 | height: min(400px, 100%); 4 | border-radius: 1rem; 5 | box-shadow: 0 0 10px -5px var(--clr-secondary); 6 | } 7 | -------------------------------------------------------------------------------- /src/features/create-tweet/components/styles/replying-to.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | color: var(--clr-tertiary); 3 | font-size: var(--fs-milli); 4 | word-wrap: break-word; 5 | word-break: break-all; 6 | 7 | .link { 8 | color: var(--clr-primary); 9 | cursor: pointer; 10 | 11 | &:focus-visible, 12 | &:hover { 13 | text-decoration: underline; 14 | } 15 | } 16 | 17 | .username { 18 | color: var(--clr-primary); 19 | cursor: pointer; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/features/create-tweet/components/styles/text-progress-bar.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 30px; 3 | height: 30px; 4 | display: grid; 5 | place-content: center; 6 | 7 | .progressbar { 8 | position: relative; 9 | height: 20px; 10 | width: 20px; 11 | 12 | svg { 13 | .progressCircle { 14 | transition: all 0.1s; 15 | } 16 | } 17 | 18 | .text { 19 | position: absolute; 20 | top: 50%; 21 | left: 50%; 22 | transform: translate(-50%, -50%); 23 | font-size: var(--fs-pico); 24 | font-weight: 300; 25 | color: var(--clr-tertiary); 26 | } 27 | 28 | .danger { 29 | color: #f4212e; 30 | } 31 | } 32 | 33 | .warning { 34 | height: 30px; 35 | width: 30px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/features/create-tweet/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/create-tweet"; 2 | export * from "./components/create-tweet-modal"; 3 | export * from "./components/create-tweet-wrapper"; 4 | export * from "./components/tweet-button"; 5 | export * from "./components/mobile-tweet-button"; 6 | export * from "./components/replying-to"; 7 | export * from "./api/post-media"; 8 | export * from "./types"; 9 | -------------------------------------------------------------------------------- /src/features/create-tweet/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Post { 2 | id: string; 3 | } 4 | 5 | export interface IChosenImages { 6 | url: string | ArrayBuffer | null; 7 | id: number; 8 | file: File; 9 | width: number; 10 | height: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/features/create-tweet/utils/resize-textarea.ts: -------------------------------------------------------------------------------- 1 | export const resizeTextarea = (textarea: HTMLTextAreaElement | null) => { 2 | if (!textarea) return; 3 | 4 | textarea.style.height = "0px"; 5 | textarea.style.height = `${textarea.scrollHeight}px`; 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/explore/api/get-hashtags.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getHashtags = async () => { 4 | try { 5 | const { data } = await axios.get(`/api/hashtags`); 6 | return data; 7 | } catch (error: any) { 8 | return error.response.data; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/explore/api/post-hashtags.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const postHashtags = async (hashtags: string[]) => { 4 | try { 5 | const { data } = await axios.post(`/api/hashtags`, { 6 | hashtags, 7 | }); 8 | return data; 9 | } catch (error: any) { 10 | return error.response.data; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/features/explore/api/retrieve-hashtags-from-tweet.ts: -------------------------------------------------------------------------------- 1 | export const retrieveHashtagsFromTweet = (text: string): string[] | null => { 2 | const hashtags = text.match(/#\w+/gi); 3 | return hashtags ? hashtags.map((hashtag) => hashtag.slice(1)) : null; 4 | }; 5 | -------------------------------------------------------------------------------- /src/features/explore/components/styles/explore.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding-bottom: calc(100vh - 4rem); 3 | 4 | .trends { 5 | border-bottom: 1px solid var(--clr-border); 6 | } 7 | 8 | .tweets { 9 | .tweet { 10 | border-bottom: 1px solid var(--clr-border); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/features/explore/hooks/use-hashtags.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { getHashtags } from "../api/get-hashtags"; 4 | import { IHashtag } from "../types"; 5 | 6 | export const useHashtags = () => { 7 | return useQuery({ 8 | queryKey: ["hashtags"], 9 | queryFn: async () => { 10 | return getHashtags(); 11 | }, 12 | 13 | refetchOnWindowFocus: false, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/explore/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/explore"; 2 | export * from "./api/retrieve-hashtags-from-tweet"; 3 | export * from "./api/post-hashtags"; 4 | export * from "./api/get-hashtags"; 5 | export * from "./hooks/use-hashtags"; 6 | export * from "./types"; 7 | -------------------------------------------------------------------------------- /src/features/explore/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IHashtag { 2 | id: string; 3 | text: string; 4 | score: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/features/font-size-customization/components/font-size-customization.tsx: -------------------------------------------------------------------------------- 1 | import { FontSizeSlider } from "./font-size-slider"; 2 | 3 | export const FontSizeCustomization = () => { 4 | return ( 5 |
6 |

10 | Font size 11 |

12 |
16 | Aa 17 | 18 | Aa 19 |
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/features/font-size-customization/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/font-size-customization"; 2 | -------------------------------------------------------------------------------- /src/features/footer/assets/three-dots-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ThreeDotsIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/footer/components/footer-link.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { ILinkProps } from "../types/index"; 4 | 5 | import styles from "./styles/link.module.scss"; 6 | 7 | export const FooterLink = ({ title = "loading", url = "#" }: ILinkProps) => { 8 | return ( 9 | 15 | {title} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/features/footer/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { ThreeDotsIcon } from "../assets/three-dots-icon"; 2 | 3 | import { FooterLink } from "./footer-link"; 4 | import styles from "./styles/footer.module.scss"; 5 | 6 | export const Footer = () => { 7 | return ( 8 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/footer/components/styles/footer.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | column-gap: 0.75em; 4 | flex-wrap: wrap; 5 | color: var(--clr-tertiary); 6 | padding: 0 1em; 7 | transition: all 0.1s ease-in-out; 8 | 9 | .moreButton { 10 | font-size: var(--fs-nano); 11 | display: flex; 12 | align-items: center; 13 | cursor: pointer; 14 | border-radius: 4px; 15 | 16 | &:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | &:focus-visible { 21 | outline: 1px solid var(--clr-secondary); 22 | } 23 | 24 | svg { 25 | width: 17px; 26 | height: 13px; 27 | fill: var(--clr-tertiary); 28 | } 29 | } 30 | 31 | span { 32 | margin: 2px 0; 33 | font-size: var(--fs-nano); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/features/footer/components/styles/link.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 2px 0; 3 | text-decoration: none; 4 | font-size: var(--fs-nano); 5 | transition: all 0.1s ease-in-out; 6 | cursor: pointer; 7 | 8 | &:hover { 9 | text-decoration: underline; 10 | } 11 | 12 | &:focus-visible { 13 | text-decoration: underline; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/features/footer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/footer"; 2 | -------------------------------------------------------------------------------- /src/features/footer/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ILinkProps { 2 | title: string; 3 | url: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/features/header/components/header.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import { cn } from "@/utils/cn"; 4 | 5 | interface IHeader extends React.HTMLAttributes { 6 | children: React.ReactNode; 7 | } 8 | 9 | export const Header: FC = ({ children, className }) => { 10 | return ( 11 |
h2]:text-h2", 14 | className, 15 | )} 16 | > 17 | {children} 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/features/header/components/tweet-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | 4 | import { BackArrowIcon } from "@/assets/back-arrow-icon"; 5 | import { Button } from "@/components/elements/button"; 6 | import { Tooltip } from "@/components/elements/tooltip"; 7 | 8 | import { Header } from "./header"; 9 | 10 | export const TweetHeader = () => { 11 | const router = useRouter(); 12 | return ( 13 |
14 | 15 | 24 | 25 |

Home

26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/profile-header"; 2 | export * from "./components/explore-header"; 3 | export * from "./components/notifications-header"; 4 | export * from "./components/tweet-header"; 5 | export * from "./components/header"; 6 | -------------------------------------------------------------------------------- /src/features/messages/api/create-conversation.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const createConversation = async ({ 4 | senderId, 5 | receiverId, 6 | }: { 7 | senderId: string | undefined; 8 | receiverId: string | null | undefined; 9 | }) => { 10 | try { 11 | const { data } = await axios.post(`/api/messages/conversations`, { 12 | sender_id: senderId, 13 | receiver_id: receiverId, 14 | }); 15 | 16 | return data?.id; 17 | } catch (error: any) { 18 | return error.message; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/features/messages/api/deleteConversation.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const deleteConversation = async ( 4 | conversationId: string | undefined, 5 | ) => { 6 | try { 7 | const { data } = await axios.delete( 8 | `/api/messages/conversations?conversation_id=${conversationId}`, 9 | ); 10 | return data; 11 | } catch (error: any) { 12 | return error.message; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/features/messages/api/get-chat.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getChat = async ({ 4 | conversation_id, 5 | pageParam, 6 | limit, 7 | }: { 8 | conversation_id: string | undefined; 9 | pageParam: string | unknown; 10 | limit: number; 11 | }) => { 12 | try { 13 | const { data } = await axios.get( 14 | `/api/messages/chat?conversation_id=${conversation_id}&cursor=${pageParam}&limit=${limit}`, 15 | ); 16 | return data; 17 | } catch (error: any) { 18 | return error.message; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/features/messages/api/get-conversation.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getConversation = async (id: string | undefined) => { 4 | try { 5 | const { data } = await axios.get(`/api/messages?conversation_id=${id}`); 6 | return data; 7 | } catch (error: any) { 8 | return error.message; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/messages/api/get-conversations.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getConversations = async (id: string | undefined) => { 4 | try { 5 | const { data } = await axios.get( 6 | `/api/messages/conversations?user_id=${id}`, 7 | ); 8 | return data; 9 | } catch (error: any) { 10 | return error.message; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/features/messages/api/post-image.ts: -------------------------------------------------------------------------------- 1 | import { createId } from "@paralleldrive/cuid2"; 2 | 3 | import { supabase } from "@/utils/supabase-client"; 4 | 5 | export const postImage = async (file: File, bucket: string) => { 6 | try { 7 | const imagePath = createId(); 8 | 9 | const { error } = await supabase.storage 10 | .from(bucket) 11 | .upload(`${bucket}-${imagePath}`, file, { 12 | cacheControl: "3600", 13 | upsert: false, 14 | }); 15 | if (error) { 16 | throw new Error(error.message); 17 | } else { 18 | const { data: mediaUrl } = supabase.storage 19 | .from(bucket) 20 | .getPublicUrl(`${bucket}-${imagePath}`); 21 | 22 | return { url: mediaUrl?.publicUrl }; 23 | } 24 | } catch (error: unknown) { 25 | throw new Error("Error uploading image"); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/features/messages/assets/arrow-down-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ArrowDownIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/messages/assets/info-icon.tsx: -------------------------------------------------------------------------------- 1 | export const InfoIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/messages/assets/send-icon.tsx: -------------------------------------------------------------------------------- 1 | export const SendIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/messages/assets/snooze-notifications-icon.tsx: -------------------------------------------------------------------------------- 1 | export const SnoozeNotificationsIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/messages/assets/user-icon.tsx: -------------------------------------------------------------------------------- 1 | export const UserIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/messages/assets/x-icon.tsx: -------------------------------------------------------------------------------- 1 | export const XICon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/messages/components/info/conversation-actions.tsx: -------------------------------------------------------------------------------- 1 | import { IUser } from "@/features/profile"; 2 | 3 | import { useDeleteConversation } from "../../hooks/use-delete-conversation"; 4 | 5 | import styles from "./styles/conversation-actions.module.scss"; 6 | 7 | export const ConversationActions = ({ 8 | member, 9 | conversationId, 10 | }: { 11 | member: IUser | undefined; 12 | conversationId: string | undefined; 13 | }) => { 14 | const mutation = useDeleteConversation(); 15 | 16 | return ( 17 |
18 | 19 | 20 | 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/features/messages/components/info/conversation-info-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | 4 | import { BackArrowIcon } from "@/assets/back-arrow-icon"; 5 | import { Button } from "@/components/elements/button"; 6 | import { Tooltip } from "@/components/elements/tooltip"; 7 | import { Header } from "@/features/header"; 8 | 9 | export const ConversationInfoHeader = () => { 10 | const router = useRouter(); 11 | 12 | return ( 13 |
14 | 15 | 24 | 25 |

Conversation info

26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/messages/components/info/styles/conversation-actions.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 4px 0; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | button { 7 | padding: 15px; 8 | text-align: center; 9 | font-size: 14px; 10 | font-weight: 300; 11 | color: var(--clr-primary); 12 | cursor: pointer; 13 | transition: background-color 0.2s ease-in-out; 14 | 15 | &:hover { 16 | background-color: var(--clr-conversation-actions-hover); 17 | } 18 | 19 | &:active { 20 | background-color: var(--clr-conversation-actions-active); 21 | } 22 | } 23 | 24 | .leave { 25 | color: var(--clr-red); 26 | 27 | &:hover { 28 | background-color: var(--clr-conversation-delete-hover); 29 | } 30 | 31 | &:active { 32 | background-color: var(--clr-conversation-delete-active); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/features/messages/components/info/styles/conversation-info.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | height: 100dvh; 4 | 5 | .members { 6 | border-bottom: 1px solid var(--clr-border); 7 | padding-bottom: 4px; 8 | } 9 | 10 | .notifications { 11 | border-bottom: 1px solid var(--clr-border); 12 | padding: 4px 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/features/messages/components/info/styles/conversation-member.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 11px 15px; 3 | display: grid; 4 | grid-template-columns: auto 1fr auto; 5 | align-items: center; 6 | gap: 8px; 7 | cursor: pointer; 8 | transition: background-color 0.2s ease-in-out; 9 | 10 | &:hover { 11 | background-color: var(--clr-tweet-hover); 12 | } 13 | 14 | .username { 15 | display: flex; 16 | align-items: center; 17 | gap: 4px; 18 | 19 | .followsYou { 20 | background-color: var(--clr-trends-hover); 21 | padding: 2px 4px; 22 | border-radius: 4px; 23 | color: var(--clr-tertiary); 24 | font-size: var(--fs-pico); 25 | white-space: nowrap; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/features/messages/components/new-message/new-message-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CloseIcon } from "@/assets/close-icon"; 3 | import { Button } from "@/components/elements/button"; 4 | import { Tooltip } from "@/components/elements/tooltip"; 5 | 6 | import { useNewMessageStore } from "../../stores/use-new-message-store"; 7 | 8 | import styles from "./styles/new-message-header.module.scss"; 9 | 10 | export const NewMessageHeader = () => { 11 | const closeModal = useNewMessageStore((state) => state.closeModal); 12 | 13 | return ( 14 |
15 | 16 | 25 | 26 | 27 |

New message

28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/features/messages/components/new-message/search-people.tsx: -------------------------------------------------------------------------------- 1 | import { SearchIcon } from "@/assets/search-icon"; 2 | 3 | import styles from "./styles/search-people.module.scss"; 4 | 5 | export const SearchPeople = ({ 6 | searchQuery, 7 | setSearchQuery, 8 | }: { 9 | searchQuery: string; 10 | setSearchQuery: (searchQuery: string) => void; 11 | }) => { 12 | return ( 13 |
14 | 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/messages/components/new-message/styles/new-message-header.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 50px; 3 | padding: 8px; 4 | display: flex; 5 | align-items: center; 6 | gap: 2rem; 7 | 8 | h1 { 9 | font-size: var(--fs-h2); 10 | font-weight: 500; 11 | color: var(--clr-secondary); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/features/messages/components/new-message/styles/search-people.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | border-bottom: 1px solid var(--clr-border); 3 | 4 | label { 5 | display: grid; 6 | grid-template-columns: auto 1fr; 7 | align-items: center; 8 | 9 | &:focus-within { 10 | .icon { 11 | svg { 12 | fill: var(--clr-primary); 13 | } 14 | } 15 | } 16 | 17 | .icon { 18 | margin-left: 16px; 19 | display: grid; 20 | place-items: center; 21 | 22 | svg { 23 | width: 17px; 24 | height: 17px; 25 | fill: var(--clr-tertiary); 26 | } 27 | } 28 | 29 | input { 30 | padding: 11px; 31 | font-size: var(--fs-milli); 32 | 33 | &::placeholder { 34 | color: var(--clr-tertiary); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/features/messages/components/start-new-conversation.tsx: -------------------------------------------------------------------------------- 1 | import { useNewMessageStore } from "../stores/use-new-message-store"; 2 | 3 | import styles from "./styles/start-new-conversation.module.scss"; 4 | 5 | export const StartNewConversation = ({ 6 | title = `Select a message`, 7 | subtitle = `Choose from your existing conversations, start a new one, or just keep swimming`, 8 | buttonText = `New Message`, 9 | }: { 10 | title?: string; 11 | subtitle?: string; 12 | buttonText?: string; 13 | }) => { 14 | const openModal = useNewMessageStore((state) => state.openModal); 15 | 16 | return ( 17 |
18 |
19 |

{title}

20 |

{subtitle}

21 | 22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/features/messages/components/styles/conversations.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | // padding: 1rem; 3 | } 4 | -------------------------------------------------------------------------------- /src/features/messages/components/styles/search-results.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .noResults { 3 | margin-top: 2rem; 4 | text-align: center; 5 | font-size: var(--fs-milli); 6 | color: var(--clr-tertiary); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/features/messages/hooks/use-create-conversation.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | import { useRouter } from "next/navigation"; 4 | 5 | export const useCreateConversation = () => { 6 | const router = useRouter(); 7 | 8 | return useMutation({ 9 | mutationFn: async ({ 10 | senderId, 11 | receiverId, 12 | }: { 13 | senderId: string | undefined; 14 | receiverId: string | null | undefined; 15 | }) => { 16 | const { data } = await axios.post(`/api/messages/conversations`, { 17 | sender_id: senderId, 18 | receiver_id: receiverId, 19 | }); 20 | return data; 21 | }, 22 | 23 | onSuccess: (data) => { 24 | router.push(`/messages/${data?.id}`); 25 | }, 26 | 27 | onError: (error) => { 28 | console.log("error", error); 29 | }, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/features/messages/hooks/use-delete-conversation.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 | import { useRouter } from "next/navigation"; 3 | 4 | import { deleteConversation } from "../api/deleteConversation"; 5 | 6 | export const useDeleteConversation = () => { 7 | const queryClient = useQueryClient(); 8 | const router = useRouter(); 9 | 10 | return useMutation({ 11 | mutationFn: async ({ 12 | conversationId, 13 | }: { 14 | conversationId: string | undefined; 15 | }) => { 16 | return deleteConversation(conversationId); 17 | }, 18 | onSuccess: () => { 19 | queryClient.invalidateQueries({ queryKey: ["conversations"] }); 20 | }, 21 | 22 | onError: (error) => { 23 | console.log("error", error); 24 | }, 25 | 26 | onSettled: () => { 27 | router.push("/messages"); 28 | }, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/features/messages/hooks/use-get-conversation.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { getConversation } from "../api/get-conversation"; 4 | import { IConversation } from "../types"; 5 | 6 | export const useGetConversation = (id: string | undefined) => { 7 | return useQuery({ 8 | queryKey: ["conversations", id], 9 | queryFn: async () => { 10 | return getConversation(id); 11 | }, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/features/messages/hooks/use-get-conversations.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { getConversations } from "../api/get-conversations"; 4 | import { IConversation } from "../types"; 5 | 6 | export const useGetConversations = (userId: string | undefined) => { 7 | return useQuery({ 8 | queryKey: ["conversations", userId], 9 | queryFn: async () => { 10 | return getConversations(userId); 11 | }, 12 | 13 | refetchInterval: 5000, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/messages/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/conversation"; 2 | export * from "./components/conversations"; 3 | export * from "./components/info/conversation-info"; 4 | export * from "./stores/use-new-message-store"; 5 | -------------------------------------------------------------------------------- /src/features/messages/stores/use-new-message-store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface INewMessageStore { 4 | isModalOpen: boolean; 5 | openModal: () => void; 6 | closeModal: () => void; 7 | } 8 | 9 | export const useNewMessageStore = create((set) => ({ 10 | isModalOpen: false, 11 | openModal: () => set({ isModalOpen: true }), 12 | closeModal: () => set({ isModalOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /src/features/messages/utils/handle-focus.ts: -------------------------------------------------------------------------------- 1 | import type { InfiniteData, QueryClient } from "@tanstack/react-query"; 2 | import type { Socket } from "socket.io-client"; 3 | 4 | import { IInfiniteChat } from "../hooks/use-get-chat"; 5 | 6 | export const handleFocus = ( 7 | queryClient: QueryClient, 8 | socket: Socket, 9 | sender_id: string | undefined, 10 | conversation_id: string | undefined, 11 | ) => { 12 | const chat = queryClient.getQueryData>([ 13 | "chat", 14 | conversation_id, 15 | ]); 16 | 17 | const lastMessage = chat?.pages?.at(0)?.chat?.at(0); 18 | if (lastMessage?.sender_id === sender_id) return; 19 | 20 | socket.emit("status", { status: "seen", message_id: lastMessage?.id }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/features/messages/utils/remove-message-from-query.ts: -------------------------------------------------------------------------------- 1 | import { InfiniteData, QueryClient } from "@tanstack/react-query"; 2 | 3 | import { IInfiniteChat } from "../hooks/use-get-chat"; 4 | 5 | export const removeMessageFromQueryData = ( 6 | messageId: string, 7 | conversation_id: string | undefined, 8 | queryClient: QueryClient, 9 | ) => { 10 | queryClient.setQueryData( 11 | ["chat", conversation_id], 12 | (oldData: InfiniteData) => { 13 | return { 14 | ...oldData, 15 | pages: oldData.pages.map((page) => ({ 16 | ...page, 17 | chat: page.chat.filter((message) => message.id !== messageId), 18 | })), 19 | }; 20 | }, 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/features/messages/utils/resend-message.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | import { Socket } from "socket.io-client"; 3 | 4 | import { IMessage } from "../types"; 5 | 6 | import { removeMessageFromQueryData } from "./remove-message-from-query"; 7 | import { updateQueryData } from "./update-query-data"; 8 | 9 | export const resendMessage = ({ 10 | message, 11 | socket, 12 | queryClient, 13 | }: { 14 | message: IMessage; 15 | socket: Socket; 16 | queryClient: QueryClient; 17 | }) => { 18 | const newMessage = { 19 | ...message, 20 | status: "sending", 21 | }; 22 | 23 | removeMessageFromQueryData(message.id, message.conversation_id, queryClient); 24 | 25 | updateQueryData(newMessage, message.conversation_id, queryClient); 26 | 27 | socket.emit("message", newMessage); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/messages/utils/scroll-into-view.ts: -------------------------------------------------------------------------------- 1 | type ScrollIntoView = { 2 | element: Element | null; 3 | behavior?: "smooth" | "auto" | "instant"; 4 | }; 5 | 6 | export const scrollIntoView = ({ 7 | element, 8 | behavior = "instant", 9 | }: ScrollIntoView) => { 10 | if (element) element.scrollIntoView({ behavior }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/features/navbar/assets/addition-icon.tsx: -------------------------------------------------------------------------------- 1 | export const AdditionIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/navbar/assets/bell-icon.tsx: -------------------------------------------------------------------------------- 1 | export const BellActive = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const Bell = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/navbar/assets/bookmark-icon.tsx: -------------------------------------------------------------------------------- 1 | export const Bookmark = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const BookmarkActive = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/navbar/assets/dashboard-icon.tsx: -------------------------------------------------------------------------------- 1 | export const DashboardIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/navbar/assets/envelope-icon.tsx: -------------------------------------------------------------------------------- 1 | export const Envelope = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const EnvelopeActive = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/navbar/assets/hashtag-icon.tsx: -------------------------------------------------------------------------------- 1 | export const Hashtag = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const HashtagActive = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/navbar/assets/home-icon.tsx: -------------------------------------------------------------------------------- 1 | export const HomeActive = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const Home = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/navbar/assets/plus-icon.tsx: -------------------------------------------------------------------------------- 1 | export const PlusIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/navbar/assets/user-icon.tsx: -------------------------------------------------------------------------------- 1 | export const User = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const UserActive = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/navbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/navbar"; 2 | export * from "./components/mobile-navbar"; 3 | export * from "./components/hamburger-menu"; 4 | -------------------------------------------------------------------------------- /src/features/navbar/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IIconProps { 2 | color: string; 3 | } 4 | 5 | export interface INavItemProps { 6 | icon: React.ReactNode; 7 | title: string; 8 | path: string; 9 | isActive: boolean; 10 | aria_label: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/features/profile/__tests__/follows-navigation.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | 3 | import { FollowsNavigation } from "../components/follows-navigation"; 4 | 5 | describe("FollowsNavigation", () => { 6 | it("renders the Followers and Following tabs", () => { 7 | render(); 8 | 9 | expect(screen.getByRole("tab", { name: "Followers" })).toBeInTheDocument(); 10 | expect(screen.getByRole("tab", { name: "Following" })).toBeInTheDocument(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/features/profile/api/follow-user.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const followUser = async (user_id: string, session_owner_id: string) => { 4 | try { 5 | const { data } = await axios.put("/api/users/follow", { 6 | user_id, 7 | session_owner_id, 8 | }); 9 | 10 | return data; 11 | } catch (error: any) { 12 | return error.message; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/features/profile/api/get-follows.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getFollows = async ( 4 | id: string | undefined, 5 | type: string | undefined, 6 | ) => { 7 | try { 8 | const { data } = await axios.get( 9 | `/api/users/follow?type=${type}&user_id=${id}`, 10 | ); 11 | return data; 12 | } catch (error: any) { 13 | return error.message; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/profile/api/get-pinned-tweet.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getPinnedTweet = async (id: string | undefined) => { 4 | try { 5 | const response = await axios.get(`/api/tweets/pin?user_id=${id}`); 6 | const pinnedTweet = response.data; 7 | return pinnedTweet; 8 | } catch (error: any) { 9 | return error.message; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/features/profile/api/get-user-likes.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getUserLikes = async (id: string | undefined) => { 4 | try { 5 | const { data } = await axios.get(`/api/tweets/likes?user_id=${id}`); 6 | return data; 7 | } catch (error: any) { 8 | return error.message; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/profile/api/get-user.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getUser = async (id: string | undefined) => { 4 | try { 5 | const response = await axios.get(`/api/users/${id}`); 6 | const user = response.data; 7 | return user; 8 | } catch (error: any) { 9 | if (error.response) { 10 | // The request was made and the server responded with a status code 11 | // that falls out of the range of 2xx 12 | console.log(error.response.data); 13 | console.log(error.response.status); 14 | console.log(error.response.headers); 15 | } else if (error.request) { 16 | // The request was made but no response was received 17 | // `error.request` is an instance of XMLHttpRequest in the browser and an instance of 18 | // http.ClientRequest in node.js 19 | console.log(error.request); 20 | } else { 21 | // Something happened in setting up the request that triggered an Error 22 | console.log("Error", error.message); 23 | } 24 | console.log(error.config); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/features/profile/api/get-users.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getUsers = async ({ 4 | id, 5 | limit, 6 | }: { 7 | id?: string; 8 | limit?: number; 9 | }) => { 10 | try { 11 | const { data } = await axios.get( 12 | `/api/users${id ? `?id=${id}` : ""}&${limit ? `&limit=${limit}` : ""}}`, 13 | ); 14 | 15 | return data; 16 | } catch (error: any) { 17 | return error.response.data; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/profile/api/post-image.ts: -------------------------------------------------------------------------------- 1 | import { createId } from "@paralleldrive/cuid2"; 2 | 3 | import { supabase } from "@/utils/supabase-client"; 4 | 5 | export const postImage = async (file: File, bucket: string) => { 6 | try { 7 | const imagePath = createId(); 8 | 9 | const { error } = await supabase.storage 10 | .from(bucket) 11 | .upload(`${bucket}-${imagePath}`, file, { 12 | cacheControl: "3600", 13 | upsert: false, 14 | }); 15 | if (error) { 16 | throw new Error(error.message); 17 | } else { 18 | const { data: mediaUrl } = supabase.storage 19 | .from(bucket) 20 | .getPublicUrl(`${bucket}-${imagePath}`); 21 | 22 | return mediaUrl?.publicUrl; 23 | } 24 | } catch (error: any) { 25 | throw new Error(error.message); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/features/profile/api/unfollow-user.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const unfollowUser = async ( 4 | user_id: string, 5 | session_owner_id: string, 6 | ) => { 7 | try { 8 | const { data } = await axios.delete( 9 | `/api/users/follow?user_id=${user_id}&session_owner_id=${session_owner_id}`, 10 | ); 11 | return data; 12 | } catch (error: any) { 13 | return error.message; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/profile/assets/calendar-icon.tsx: -------------------------------------------------------------------------------- 1 | export const CalendarIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/profile/assets/camera-icon.tsx: -------------------------------------------------------------------------------- 1 | export const CameraIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/profile/assets/website-icon.tsx: -------------------------------------------------------------------------------- 1 | export const WebsiteIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/profile/components/avatar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React from "react"; 3 | 4 | import { cn } from "@/utils/cn"; 5 | 6 | interface IAvatar extends React.ImgHTMLAttributes { 7 | userImage: string | null; 8 | className?: string; 9 | width?: number; 10 | height?: number; 11 | } 12 | 13 | export const Avatar = ({ userImage, className, ...props }: IAvatar) => { 14 | return ( 15 |
21 | profile picture 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/features/profile/components/follows-link.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import styles from "./styles/follows-link.module.scss"; 4 | 5 | export const FollowsLink = ({ 6 | stats, 7 | text, 8 | link, 9 | onClick, 10 | }: { 11 | stats: number; 12 | text: string; 13 | link: string; 14 | onClick?: () => void; 15 | }) => { 16 | return ( 17 | 18 | {stats} {text} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/features/profile/components/follows-navigation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePathname } from "next/navigation"; 3 | 4 | import { NavigationTab } from "./navigation-tab"; 5 | import styles from "./styles/follows-navigation.module.scss"; 6 | 7 | export const FollowsNavigation = () => { 8 | const pathname = usePathname(); 9 | const id = pathname?.split("/")[1]; 10 | 11 | return ( 12 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/profile/components/link-to-profile.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import styles from "./styles/link-to-profile.module.scss"; 4 | 5 | export const LinkToProfile = ({ 6 | userId, 7 | tabIndex = 0, 8 | onClick, 9 | children, 10 | }: { 11 | userId: string | undefined; 12 | tabIndex?: number; 13 | onClick?: () => void; 14 | children: React.ReactNode; 15 | }) => { 16 | return ( 17 | { 19 | e.stopPropagation(); 20 | if (onClick) { 21 | onClick(); 22 | } 23 | }} 24 | className={styles.container} 25 | tabIndex={tabIndex} 26 | href={`/${userId}`} 27 | > 28 | {children} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/features/profile/components/navigation-tab.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { UseFocusOnActiveTab } from "../hooks/use-focus-on-active-tab"; 4 | import { handleNavigationInteraction } from "../utils/handle-navigation-interaction"; 5 | 6 | import styles from "./styles/navigation-tab.module.scss"; 7 | 8 | export const NavigationTab = ({ 9 | text, 10 | href, 11 | active, 12 | }: { 13 | text: string; 14 | href: string; 15 | active: boolean; 16 | }) => { 17 | UseFocusOnActiveTab({ 18 | path: href, 19 | }); 20 | 21 | return ( 22 | { 31 | handleNavigationInteraction({ 32 | e, 33 | path: href, 34 | }); 35 | }} 36 | > 37 | {text} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/features/profile/components/no-followers.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import styles from "./styles/no-followers.module.scss"; 4 | 5 | export const NoFollowers = ({ 6 | title, 7 | subtitle, 8 | }: { 9 | title: string; 10 | subtitle: string; 11 | }) => { 12 | return ( 13 |
14 |
15 | {`no 21 |

{title}

22 |

{subtitle}

23 |
24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/features/profile/components/pinned-tweet.tsx: -------------------------------------------------------------------------------- 1 | import { Tweet } from "@/features/tweets"; 2 | 3 | import { usePinnedTweet } from "../hooks/use-pinned-tweet"; 4 | 5 | export const PinnedTweet = ({ userId }: { userId: string | undefined }) => { 6 | const { data: pinnedTweet } = usePinnedTweet(userId); 7 | 8 | if (!pinnedTweet) return null; 9 | 10 | return ( 11 |
16 | 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/profile/components/profile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePathname } from "next/navigation"; 3 | 4 | import { LoadingSpinner } from "@/components/elements/loading-spinner"; 5 | import { TryAgain } from "@/components/elements/try-again"; 6 | import { 7 | IUser, 8 | ProfileInfo, 9 | ProfileNavigation, 10 | useUser, 11 | } from "@/features/profile"; 12 | 13 | export const Profile = ({ initialUser }: { initialUser: IUser }) => { 14 | const pathname = usePathname(); 15 | const id = pathname?.split("/")[1]; 16 | 17 | const { 18 | data: user, 19 | isError, 20 | status, 21 | } = useUser({ 22 | id, 23 | initialData: initialUser, 24 | }); 25 | 26 | if (status === "pending") { 27 | return ; 28 | } 29 | 30 | if (status === "error" || isError) { 31 | return ; 32 | } 33 | 34 | return ( 35 | <> 36 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/follows-link.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | font-size: var(--fs-micro); 3 | font-weight: var(--fw-400); 4 | color: var(--clr-secondary); 5 | cursor: pointer; 6 | 7 | &:hover { 8 | text-decoration: underline; 9 | } 10 | 11 | &:focus-visible { 12 | text-decoration: underline; 13 | } 14 | 15 | strong { 16 | font-weight: var(--fw-700); 17 | } 18 | 19 | span { 20 | color: var(--clr-tertiary); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/follows-navigation.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | grid-auto-flow: column; 4 | border-bottom: 1px solid var(--clr-border); 5 | } 6 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/inspect-image-modal.module.scss: -------------------------------------------------------------------------------- 1 | .close { 2 | position: fixed; 3 | top: 0.5rem; 4 | left: 0.5rem; 5 | } 6 | 7 | .banner { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | bottom: 0; 13 | margin: auto; 14 | width: 100%; 15 | aspect-ratio: 3 / 1; 16 | overflow: hidden; 17 | 18 | img { 19 | width: 100%; 20 | height: 100%; 21 | object-fit: cover; 22 | } 23 | } 24 | 25 | .avatar { 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | right: 0; 30 | bottom: 0; 31 | margin: auto; 32 | width: 100%; 33 | max-width: 400px; 34 | aspect-ratio: 1 / 1; 35 | border-radius: 100vmax; 36 | overflow: hidden; 37 | 38 | img { 39 | width: 100%; 40 | height: 100%; 41 | object-fit: cover; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/link-to-profile.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | cursor: pointer; 3 | 4 | &:focus-visible { 5 | span { 6 | text-decoration: underline; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/no-followers.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | place-items: center; 4 | padding: 30px; 5 | 6 | .content { 7 | max-width: 320px; 8 | 9 | img { 10 | margin-bottom: 20px; 11 | width: 100%; 12 | height: 100%; 13 | object-fit: cover; 14 | } 15 | 16 | h1 { 17 | font-size: var(--fs-kilo); 18 | font-weight: var(--fw-800); 19 | letter-spacing: 0.8px; 20 | color: var(--clr-secondary); 21 | margin-bottom: 8px; 22 | } 23 | 24 | p { 25 | font-size: var(-fs-milli); 26 | color: var(--clr-tertiary); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/profile-likes.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .noLikes { 3 | display: flex; 4 | justify-content: center; 5 | padding: 2rem; 6 | 7 | div { 8 | max-width: 340px; 9 | display: flex; 10 | flex-direction: column; 11 | gap: 1rem; 12 | 13 | h1 { 14 | font-size: var(--fs-kilo); 15 | font-weight: var(--fw-700); 16 | color: var(--clr-secondary); 17 | word-wrap: break-word; 18 | } 19 | 20 | p { 21 | font-size: var(--fs-milli); 22 | color: var(--clr-tertiary); 23 | } 24 | } 25 | } 26 | 27 | .tweetContainer { 28 | border-bottom: 1px solid var(--clr-border); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/profile-media.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .noMedia { 3 | display: flex; 4 | justify-content: center; 5 | padding: 2rem; 6 | 7 | img { 8 | width: 100%; 9 | height: 100%; 10 | object-fit: cover; 11 | } 12 | 13 | div { 14 | max-width: 340px; 15 | display: flex; 16 | flex-direction: column; 17 | gap: 1rem; 18 | 19 | h1 { 20 | font-size: var(--fs-kilo); 21 | font-weight: var(--fw-700); 22 | color: var(--clr-secondary); 23 | word-wrap: break-word; 24 | } 25 | 26 | p { 27 | font-size: var(--fs-milli); 28 | color: var(--clr-tertiary); 29 | } 30 | } 31 | } 32 | 33 | .tweetContainer { 34 | border-bottom: 1px solid var(--clr-border); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/profile-navigation.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | grid-auto-flow: column; 4 | border-bottom: 1px solid var(--clr-border); 5 | } 6 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/profile-tweets-and-replies.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .noTweets { 3 | display: flex; 4 | justify-content: center; 5 | padding: 2rem; 6 | 7 | div { 8 | max-width: 340px; 9 | display: flex; 10 | flex-direction: column; 11 | gap: 1rem; 12 | 13 | h1 { 14 | font-size: var(--fs-kilo); 15 | font-weight: var(--fw-700); 16 | color: var(--clr-secondary); 17 | word-wrap: break-word; 18 | } 19 | 20 | p { 21 | font-size: var(--fs-milli); 22 | color: var(--clr-tertiary); 23 | } 24 | } 25 | } 26 | 27 | .tweetContainer { 28 | border-bottom: 1px solid var(--clr-border); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/profile-tweets.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .noTweets { 3 | display: flex; 4 | justify-content: center; 5 | padding: 2rem; 6 | 7 | div { 8 | max-width: 340px; 9 | display: flex; 10 | flex-direction: column; 11 | gap: 1rem; 12 | 13 | h1 { 14 | font-size: var(--fs-kilo); 15 | font-weight: 700; 16 | color: var(--clr-secondary); 17 | word-wrap: break-word; 18 | } 19 | 20 | p { 21 | font-size: var(--fs-milli); 22 | color: var(--clr-tertiary); 23 | } 24 | } 25 | } 26 | 27 | .tweetContainer { 28 | border-bottom: 1px solid var(--clr-border); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/profile.module.scss: -------------------------------------------------------------------------------- 1 | .loading, 2 | .error { 3 | padding-top: 2rem; 4 | } 5 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/user-join-date.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | font-size: var(--fs-milli); 3 | color: var(--clr-tertiary); 4 | display: flex; 5 | align-items: flex-end; 6 | 7 | svg { 8 | width: var(--fs-h3); 9 | height: var(--fs-h3); 10 | fill: var(--clr-tertiary); 11 | } 12 | 13 | .text { 14 | margin-left: 4px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/user-modal-wrapper.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | } 4 | -------------------------------------------------------------------------------- /src/features/profile/components/styles/user-screen-name.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | font-size: var(--fs-milli); 3 | color: var(--clr-tertiary); 4 | } 5 | -------------------------------------------------------------------------------- /src/features/profile/components/user-join-date.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | 3 | import { CalendarIcon } from "../assets/calendar-icon"; 4 | 5 | import styles from "./styles/user-join-date.module.scss"; 6 | 7 | export const UserJoinDate = ({ 8 | date, 9 | showIcon = true, 10 | }: { 11 | date: Date | undefined; 12 | showIcon?: boolean; 13 | }) => { 14 | return ( 15 |
16 | {showIcon && } 17 | 18 | Joined {dayjs(date).format("MMMM YYYY")} 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/features/profile/components/user-name.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { VerifiedIcon } from "@/assets/verified-icon"; 4 | import { cn } from "@/utils/cn"; 5 | 6 | interface IUserName extends React.HTMLAttributes { 7 | name: string | undefined; 8 | isVerified?: boolean | undefined; 9 | hover?: boolean | undefined; 10 | } 11 | 12 | export const UserName = ({ 13 | name, 14 | isVerified = false, 15 | hover = false, 16 | className, 17 | }: IUserName) => { 18 | return ( 19 |
26 | {name && {name}} 27 | {isVerified && } 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/features/profile/components/user-screen-name.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./styles/user-screen-name.module.scss"; 2 | 3 | export const UserScreenName = ({ 4 | screenName, 5 | }: { 6 | screenName: string | undefined; 7 | }) => { 8 | return @{screenName}; 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/profile/hooks/use-focus-on-active-tab.tsx: -------------------------------------------------------------------------------- 1 | import { deleteCookie, getCookie } from "cookies-next"; 2 | import { useEffect } from "react"; 3 | 4 | export const UseFocusOnActiveTab = ({ path }: { path: string }) => { 5 | useEffect(() => { 6 | const tab = getCookie("tab"); 7 | 8 | if (tab) { 9 | const element = document.querySelector( 10 | `div[role="tablist"] a[href="${tab}"]`, 11 | ) as HTMLElement | null; 12 | 13 | if (element) { 14 | element.focus(); 15 | deleteCookie("tab"); 16 | } 17 | } 18 | 19 | return () => {}; 20 | }, [path]); 21 | }; 22 | -------------------------------------------------------------------------------- /src/features/profile/hooks/use-follow.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 | 3 | import { followUser } from "../api/follow-user"; 4 | import { unfollowUser } from "../api/unfollow-user"; 5 | 6 | export const useFollow = (type: "follow" | "unfollow") => { 7 | const queryClient = useQueryClient(); 8 | 9 | return useMutation({ 10 | mutationFn: ({ 11 | user_id, 12 | session_owner_id, 13 | }: { 14 | user_id: string; 15 | session_owner_id: string; 16 | }) => { 17 | return type === "follow" 18 | ? followUser(user_id, session_owner_id) 19 | : unfollowUser(user_id, session_owner_id); 20 | }, 21 | 22 | onSuccess: () => { 23 | console.log("success"); 24 | }, 25 | 26 | onError: () => { 27 | console.log("error"); 28 | }, 29 | 30 | onSettled: () => { 31 | queryClient.invalidateQueries({ queryKey: ["users"] }); 32 | }, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/features/profile/hooks/use-get-follows.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { getFollows } from "../api/get-follows"; 4 | import { IUser } from "../types"; 5 | 6 | export const useGetFollows = ({ 7 | id, 8 | type, 9 | }: { 10 | id: string | undefined; 11 | type: string | undefined; 12 | }) => { 13 | return useQuery({ 14 | queryKey: ["users", id, type], 15 | queryFn: async () => { 16 | return getFollows(id, type); 17 | }, 18 | refetchOnWindowFocus: false, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/features/profile/hooks/use-pinned-tweet.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { ITweet } from "@/features/tweets"; 4 | 5 | import { getPinnedTweet } from "../api/get-pinned-tweet"; 6 | 7 | export const usePinnedTweet = (id: string | undefined) => { 8 | return useQuery({ 9 | queryKey: ["tweets", { userId: id }, `pinned`], 10 | queryFn: async () => { 11 | return getPinnedTweet(id); 12 | }, 13 | refetchOnWindowFocus: false, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/profile/hooks/use-user-likes.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { ILike } from "@/features/tweets"; 4 | 5 | import { getUserLikes } from "../api/get-user-likes"; 6 | 7 | export const useUserLikes = (id: string | undefined) => { 8 | return useQuery({ 9 | queryKey: ["likes", { userId: id }], 10 | queryFn: async () => { 11 | return getUserLikes(id); 12 | }, 13 | refetchOnWindowFocus: false, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/profile/hooks/use-user.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { getUser } from "../api/get-user"; 4 | import { IUser } from "../types"; 5 | 6 | export const useUser = ({ 7 | id, 8 | initialData, 9 | }: { 10 | id: string | undefined; 11 | initialData?: IUser; 12 | }) => { 13 | return useQuery({ 14 | queryKey: ["users", id], 15 | queryFn: async () => { 16 | return getUser(id); 17 | }, 18 | refetchOnWindowFocus: false, 19 | initialData: initialData ?? undefined, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/features/profile/hooks/use-users.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { useSession } from "next-auth/react"; 3 | 4 | import { getUsers } from "../api/get-users"; 5 | import { IUser } from "../types"; 6 | 7 | export const useUsers = ({ 8 | queryKey, 9 | limit, 10 | }: { 11 | queryKey: string[]; 12 | limit?: number; 13 | }) => { 14 | const { data: session } = useSession(); 15 | 16 | return useQuery({ 17 | // eslint-disable-next-line @tanstack/query/exhaustive-deps 18 | queryKey, 19 | queryFn: async () => { 20 | return getUsers({ id: session?.user?.id, limit }); 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/features/profile/types/index.ts: -------------------------------------------------------------------------------- 1 | import { User, Like } from "@prisma/client"; 2 | 3 | import { ITweet } from "@/features/tweets"; 4 | import { IBookmark } from "@/features/tweets"; 5 | 6 | export interface IUser extends User { 7 | tweets: ITweet[]; 8 | followers: User[]; 9 | following: User[]; 10 | likes: ILike[]; 11 | bookmarks: IBookmark[]; 12 | pinned_tweet: ITweet; 13 | _count?: { 14 | followers?: number; 15 | following?: number; 16 | tweets?: number; 17 | likes?: number; 18 | }; 19 | } 20 | 21 | export interface IProfile { 22 | name: string; 23 | bio: string | undefined; 24 | location: string | undefined; 25 | website: string | undefined; 26 | banner: { 27 | url: string | undefined; 28 | file: File | undefined; 29 | }; 30 | avatar: { 31 | url: string | undefined; 32 | file: File | undefined; 33 | }; 34 | } 35 | 36 | export interface ILike extends Like { 37 | user: IUser; 38 | tweet: ITweet; 39 | } 40 | -------------------------------------------------------------------------------- /src/features/profile/utils/following.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from "../types"; 2 | 3 | export const following = ({ 4 | user, 5 | session_owner_id, 6 | }: { 7 | user: IUser | undefined; 8 | session_owner_id: string; 9 | }): boolean => { 10 | return user 11 | ? user?.followers?.some((follower) => follower.id === session_owner_id) 12 | : false; 13 | }; 14 | -------------------------------------------------------------------------------- /src/features/profile/utils/handle-navigation-interaction.ts: -------------------------------------------------------------------------------- 1 | import { setCookie } from "cookies-next"; 2 | 3 | export const handleNavigationInteraction = ({ 4 | e, 5 | path, 6 | }: { 7 | e: React.KeyboardEvent; 8 | path: string; 9 | }) => { 10 | if (e.key === "ArrowRight") { 11 | if (!e.currentTarget.nextElementSibling) { 12 | ( 13 | e.currentTarget?.parentElement?.firstElementChild as HTMLElement 14 | )?.focus(); 15 | } else (e.currentTarget.nextElementSibling as HTMLElement)?.focus(); 16 | } 17 | if (e.key === "ArrowLeft") { 18 | if (!e.currentTarget.previousElementSibling) { 19 | ( 20 | e.currentTarget?.parentElement?.lastElementChild as HTMLElement 21 | )?.focus(); 22 | } else (e.currentTarget.previousElementSibling as HTMLElement)?.focus(); 23 | } 24 | 25 | if (e.key === "Enter") { 26 | if (e.currentTarget.ariaSelected === "true") { 27 | return; 28 | } 29 | 30 | e.currentTarget.click(); 31 | setCookie("tab", path); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/features/search/api/get-query-people.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getQueryPeople = async (query: string | undefined) => { 4 | try { 5 | const { data } = await axios.get(`/api/search/people?query=${query}`); 6 | return data; 7 | } catch (error: any) { 8 | return error.Message; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/search/api/get-search-results.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getSearchResults = async (query: string) => { 4 | try { 5 | const { data } = await axios.get(`/api/search?query=${query}`); 6 | return data; 7 | } catch (error: any) { 8 | return error.message; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/search/assets/search-close-icon.tsx: -------------------------------------------------------------------------------- 1 | export const SearchCloseIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/search/assets/search-icon.tsx: -------------------------------------------------------------------------------- 1 | export const SearchIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/search/components/no-results.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | import styles from "./styles/no-results.module.scss"; 5 | 6 | export const NoResults = ({ query }: { query: string | undefined }) => { 7 | return ( 8 |
9 |
10 |
11 | no results 18 |
19 |

No results for "{query}"

20 |

21 | Try searching for something else, or check your{" "} 22 | Search settings to see if they’re 23 | protecting you from potentially sensitive content. 24 |

25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/search/components/styles/no-results.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | place-items: center; 4 | 5 | .content { 6 | width: 100%; 7 | max-width: 380px; 8 | padding: 30px; 9 | 10 | .image { 11 | margin-bottom: 30px; 12 | max-width: 320px; 13 | width: 100%; 14 | 15 | img { 16 | width: 100%; 17 | height: 100%; 18 | object-fit: cover; 19 | } 20 | } 21 | 22 | h1 { 23 | font-size: var(--fs-kilo); 24 | font-weight: var(--fw-700); 25 | color: var(--clr-secondary); 26 | margin-bottom: 8px; 27 | word-wrap: break-word; 28 | word-break: break-all; 29 | } 30 | 31 | p { 32 | font-size: var(--fs-milli); 33 | color: var(--clr-tertiary); 34 | margin-bottom: 30px; 35 | line-height: 1.4; 36 | 37 | a { 38 | color: var(--clr-primary); 39 | cursor: pointer; 40 | 41 | &:hover { 42 | text-decoration: underline; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/features/search/components/styles/search-results.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding-bottom: calc(100vh - 8rem); 3 | 4 | .people { 5 | border-top: 1px solid var(--clr-border); 6 | border-bottom: 1px solid var(--clr-border); 7 | 8 | h1 { 9 | padding: 0.8rem 1rem; 10 | font-size: var(--fs-h2); 11 | font-weight: var(--fw-700); 12 | } 13 | 14 | .viewAll { 15 | padding: 1em; 16 | width: 100%; 17 | text-align: left; 18 | border: none; 19 | background: none; 20 | cursor: pointer; 21 | font-size: var(--fs-milli); 22 | color: var(--clr-primary); 23 | font-weight: 300; 24 | 25 | &:hover { 26 | background-color: var(--clr-tweet-hover); 27 | } 28 | 29 | &:focus-visible { 30 | outline: 2px solid var(--clr-primary-disabled); 31 | outline-offset: -2px; 32 | } 33 | } 34 | } 35 | 36 | .tweets { 37 | .tweetContainer { 38 | border-bottom: 1px solid var(--clr-border); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/features/search/hooks/use-search-people.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { IUser } from "@/features/profile"; 4 | 5 | import { getQueryPeople } from "../api/get-query-people"; 6 | 7 | export const useSearchPeople = (query: string | undefined) => { 8 | return useQuery({ 9 | queryKey: ["people", "query: ", query], 10 | queryFn: async () => { 11 | return getQueryPeople(query); 12 | }, 13 | refetchOnWindowFocus: false, 14 | 15 | enabled: !!query, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/features/search/hooks/use-search.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { IHashtag } from "@/features/explore"; 4 | import { IUser } from "@/features/profile"; 5 | 6 | import { getSearchResults } from "../api/get-search-results"; 7 | 8 | export const useSearch = (query: string) => { 9 | return useQuery<{ 10 | people: IUser[]; 11 | hashtags: IHashtag[]; 12 | }>({ 13 | queryKey: ["search", query], 14 | queryFn: async () => { 15 | return getSearchResults(query); 16 | }, 17 | refetchOnWindowFocus: false, 18 | enabled: !!query, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/features/search/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/search"; 2 | export * from "./components/search-results"; 3 | export * from "./components/search-header"; 4 | export * from "./stores/use-search"; 5 | -------------------------------------------------------------------------------- /src/features/search/stores/use-search.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface ISearchStore { 4 | query: string; 5 | setQuery: (query: string) => void; 6 | isResultsModalOpen: boolean; 7 | openResultsModal: () => void; 8 | closeResultsModal: () => void; 9 | } 10 | 11 | export const useSearchStore = create((set) => ({ 12 | query: "", 13 | setQuery: (query: string) => set({ query }), 14 | isResultsModalOpen: false, 15 | openResultsModal: () => set({ isResultsModalOpen: true }), 16 | closeResultsModal: () => set({ isResultsModalOpen: false }), 17 | })); 18 | -------------------------------------------------------------------------------- /src/features/search/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ISearch { 2 | id: number; 3 | text: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/features/sidebar/assets/logo-icon.tsx: -------------------------------------------------------------------------------- 1 | export const LogoIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/sidebar/assets/options-icon.tsx: -------------------------------------------------------------------------------- 1 | export const OptionsIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/sidebar/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { LogoIcon } from "../assets/logo-icon"; 4 | 5 | import styles from "./styles/logo.module.scss"; 6 | 7 | export const Logo = () => { 8 | return ( 9 |

10 | 11 | 12 | 13 |

14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/sidebar/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSession } from "next-auth/react"; 3 | 4 | import { SessionOwnerButton } from "@/features/auth"; 5 | import { TweetButton } from "@/features/create-tweet"; 6 | import { Navbar } from "@/features/navbar"; 7 | 8 | import { Logo } from "./logo"; 9 | import styles from "./styles/sidebar.module.scss"; 10 | 11 | export const Sidebar = () => { 12 | const { data: session } = useSession(); 13 | 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 | {session && ( 23 |
24 | 25 |
26 | )} 27 | {session && ( 28 |
29 | 30 |
31 | )} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/features/sidebar/components/styles/logo.module.scss: -------------------------------------------------------------------------------- 1 | @use "abstracts/media-query.scss" as *; 2 | 3 | .container { 4 | border-radius: 100vmax; 5 | cursor: pointer; 6 | transition: all 0.1s ease-in-out; 7 | 8 | a { 9 | padding: 0.75em; 10 | display: grid; 11 | } 12 | 13 | &:hover { 14 | background-color: var(--clr-logo-hover); 15 | } 16 | 17 | &:active { 18 | background-color: var(--clr-logo-active); 19 | } 20 | 21 | &:has(:focus-visible) { 22 | outline: 2px solid var(--clr-secondary); 23 | background-color: var(--clr-logo-hover); 24 | } 25 | 26 | svg { 27 | height: var(--logo-size); 28 | width: var(--logo-size); 29 | fill: var(--clr-logo); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/features/sidebar/components/styles/sidebar.module.scss: -------------------------------------------------------------------------------- 1 | @use "abstracts/media-query.scss" as *; 2 | 3 | .container { 4 | background-color: var(--clr-background); 5 | position: sticky; 6 | top: 0; 7 | height: 100vh; 8 | height: 100dvh; 9 | overflow-y: auto; 10 | display: none; 11 | 12 | .logo, 13 | .tweetButton, 14 | .user { 15 | display: flex; 16 | justify-content: center; 17 | } 18 | 19 | .user { 20 | margin-top: auto; 21 | } 22 | 23 | @include mq("small") { 24 | display: grid; 25 | grid-template-rows: min-content min-content min-content 1fr; 26 | padding: 4px; 27 | } 28 | 29 | @include mq("medium") { 30 | padding: 4px 0.5em; 31 | } 32 | 33 | @include mq("xx-large") { 34 | .logo, 35 | .tweetButton, 36 | .user { 37 | justify-content: flex-start; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/features/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/sidebar"; 2 | export * from "../auth/components/session-owner-modal"; 3 | export * from "../auth/components/signout-modal"; 4 | -------------------------------------------------------------------------------- /src/features/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/theme-picker"; 2 | -------------------------------------------------------------------------------- /src/features/trends/components/styles/trends.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .loading, 3 | .error { 4 | padding: 0.4rem; 5 | } 6 | 7 | h2 { 8 | color: var(--clr-secondary); 9 | font-size: var(--fs-h2); 10 | font-weight: var(--fw-700); 11 | padding: 0.75rem 1rem; 12 | } 13 | 14 | .showMore { 15 | display: block; 16 | padding: 1em; 17 | color: var(--clr-primary); 18 | cursor: pointer; 19 | font-size: var(--fs-base); 20 | border-radius: 0 0 1rem 1rem; 21 | outline-offset: -2px; 22 | 23 | &:hover { 24 | background-color: var(--clr-trends-hover); 25 | } 26 | 27 | &:active { 28 | background-color: var(--clr-trends-active); 29 | } 30 | 31 | &:focus-visible { 32 | outline: 2px solid var(--clr-primary-disabled); 33 | background-color: var(--clr-trends-hover); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/features/trends/components/trends-fallback.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { Button } from "@/components/elements/button"; 4 | import { IErrorFallback } from "@/components/elements/error-fallback/error-fallback"; 5 | import { ReloadIcon } from "@/components/elements/try-again/assets/reload-icon"; 6 | 7 | export const TrendsFallback: FC = ({ 8 | error, 9 | resetErrorBoundary, 10 | }) => { 11 | return ( 12 |
13 |

{error.message}

14 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/features/trends/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/trends"; 2 | export * from "./components/trends-fallback"; 3 | export * from "./components/trend"; 4 | export * from "./components/trends-header"; 5 | -------------------------------------------------------------------------------- /src/features/trends/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface iTrendProps { 2 | ranking: number; 3 | title: string; 4 | tweets: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/features/tweets/api/delete-tweet.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const deleteTweet = async (tweetId: string) => { 4 | try { 5 | const { data } = await axios.delete(`/api/tweets?id=${tweetId}`); 6 | 7 | return data; 8 | } catch (error: any) { 9 | console.log(error); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/features/tweets/api/get-tweet.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default async function getTweet(id: string | undefined) { 4 | try { 5 | const { data } = await axios.get(`/api/tweets/${id}`); 6 | return data; 7 | } catch (error: any) { 8 | console.error(error); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/features/tweets/api/get-tweets.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | export const getTweets = async ({ 3 | pageParam = "", 4 | limit = 20, 5 | type, 6 | id, 7 | }: { 8 | pageParam?: string | unknown; 9 | limit?: number; 10 | type?: string; 11 | id?: string; 12 | }) => { 13 | try { 14 | const { data } = await axios.get( 15 | `/api/tweets?cursor=${pageParam}&limit=${limit}${ 16 | type ? `&type=${type}` : "" 17 | }${id ? `&id=${id}` : ""}`, 18 | ); 19 | return data; 20 | } catch (error: any) { 21 | return error.response.data; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/features/tweets/api/handle-retweet.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const handleRetweet = async (tweetId: string, userId: string) => { 4 | try { 5 | const { data } = await axios.post(`/api/tweets/retweets`, { 6 | tweet_id: tweetId, 7 | user_id: userId, 8 | }); 9 | return data; 10 | } catch (error: any) { 11 | return error.response.data; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/features/tweets/api/pin-tweet.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const pinTweet = async ( 4 | tweetId: string | undefined, 5 | userId: string | undefined, 6 | ) => { 7 | try { 8 | const { data } = await axios.post("/api/tweets/pin", { 9 | tweet_id: tweetId, 10 | user_id: userId, 11 | }); 12 | return data; 13 | } catch (error: any) { 14 | return error.response.data; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/features/tweets/api/toggle-like.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | export const toggleLike = async ({ 3 | tweetId, 4 | userId, 5 | }: { 6 | tweetId: string | undefined; 7 | userId: string | undefined; 8 | }) => { 9 | try { 10 | const response = await axios.post("/api/tweets/likes", { 11 | tweet_id: tweetId, 12 | user_id: userId, 13 | }); 14 | const data = response.data; 15 | return data; 16 | } catch (error: any) { 17 | return error.message; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/tweets/api/unpin-tweet.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const unpinTweet = async (id: string | undefined) => { 4 | try { 5 | const { data } = await axios.put("/api/tweets/pin", { 6 | id, 7 | }); 8 | return data; 9 | } catch (error: any) { 10 | if (error.response) { 11 | return error.response.data; 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/features/tweets/assets/backward-arrow-icon.tsx: -------------------------------------------------------------------------------- 1 | export const BackwardArrowIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/tweets/assets/block-icon.tsx: -------------------------------------------------------------------------------- 1 | export const BlockIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const UnBlockIcon = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/tweets/assets/bookmark-icon.tsx: -------------------------------------------------------------------------------- 1 | export const AddToBookmarksIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const RemoveFromBookmarksIcon = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/tweets/assets/copy-link-icon.tsx: -------------------------------------------------------------------------------- 1 | export const CopyLinkIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/tweets/assets/double-arrows-icon.tsx: -------------------------------------------------------------------------------- 1 | export const HideArrowsIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const ShowArrowsIcon = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/tweets/assets/edit-icon.tsx: -------------------------------------------------------------------------------- 1 | export const EditIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/tweets/assets/embed-icon.tsx: -------------------------------------------------------------------------------- 1 | export const EmbedIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/tweets/assets/forward-arrow-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ForwardArrowIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/tweets/assets/mute-icon.tsx: -------------------------------------------------------------------------------- 1 | export const MuteIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | export const UnmuteIcon = () => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/tweets/assets/pin-icon.tsx: -------------------------------------------------------------------------------- 1 | export const PinIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/tweets/assets/quote-tweet-icon.tsx: -------------------------------------------------------------------------------- 1 | export const QuoteTweetIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/tweets/assets/share-icon.tsx: -------------------------------------------------------------------------------- 1 | export const ShareIcon = () => { 2 | return ( 3 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/features/tweets/components/styles/comments.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .tweetContainer { 3 | border-bottom: 1px solid var(--clr-border); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/features/tweets/components/styles/infinite-tweets.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .tweetContainer { 3 | border-bottom: 1px solid var(--clr-border); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/features/tweets/components/styles/tweet-actions.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 0 auto; 3 | padding: 0.2em 0; 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | max-width: 600px; 8 | 9 | span { 10 | transition: background 0.1s ease-in; 11 | } 12 | } 13 | 14 | .tweet { 15 | justify-content: space-between; 16 | 17 | svg { 18 | width: var(--fs-h3); 19 | height: var(--fs-h3); 20 | } 21 | } 22 | 23 | .tweetDetails { 24 | justify-content: space-around; 25 | 26 | svg { 27 | width: var(--fs-h1); 28 | height: var(--fs-h1); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/features/tweets/components/styles/tweet-author.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | gap: 1rem; 5 | margin-bottom: 1rem; 6 | 7 | .options { 8 | margin-left: auto; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/features/tweets/components/styles/tweet-quotes.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .quote { 3 | border-bottom: 1px solid var(--clr-border); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/features/tweets/components/styles/tweet-statistics.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 1rem 0.2rem; 3 | border-bottom: 1px solid var(--clr-border); 4 | display: flex; 5 | align-items: center; 6 | gap: 1.2rem; 7 | flex-wrap: wrap; 8 | 9 | .statistic { 10 | font-size: var(--fs-micro); 11 | color: var(--clr-secondary); 12 | 13 | strong { 14 | font-weight: var(--fw-700); 15 | } 16 | 17 | span { 18 | color: var(--clr-tertiary); 19 | } 20 | } 21 | 22 | button, 23 | a { 24 | cursor: pointer; 25 | 26 | &:hover { 27 | text-decoration: underline; 28 | } 29 | 30 | &:focus-visible { 31 | text-decoration: underline; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/features/tweets/components/styles/tweets.module.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davitJabushanuri/Chirp/4e1c4aed54c04af34598bfd4157005313155ccd1/src/features/tweets/components/styles/tweets.module.scss -------------------------------------------------------------------------------- /src/features/tweets/components/tweet-creation-date.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import Link from "next/link"; 3 | 4 | import { Tooltip } from "@/components/elements/tooltip"; 5 | 6 | export const TweetCreationDate = ({ 7 | date, 8 | link = "#", 9 | }: { 10 | date: Date | undefined; 11 | link?: string; 12 | }) => { 13 | const created = dayjs(date); 14 | 15 | return ( 16 |
17 | 18 | 23 | 26 | 27 | 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/features/tweets/components/tweets.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { LoadingSpinner } from "@/components/elements/loading-spinner"; 4 | import { TryAgain } from "@/components/elements/try-again"; 5 | 6 | import { useTweets } from "../hooks/use-tweets"; 7 | 8 | import { InfiniteTweets } from "./infinite-tweets"; 9 | 10 | export const Tweets = () => { 11 | const { 12 | data: tweets, 13 | isLoading, 14 | isError, 15 | isSuccess, 16 | isFetchingNextPage, 17 | fetchNextPage, 18 | hasNextPage, 19 | } = useTweets({}); 20 | 21 | if (isLoading) { 22 | return ; 23 | } 24 | 25 | if (isError) { 26 | return ; 27 | } 28 | 29 | return ( 30 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/features/tweets/hooks/use-delete-tweet.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 | import { usePathname, useRouter } from "next/navigation"; 3 | import { toast } from "react-toastify"; 4 | 5 | import { deleteTweet } from "../api/delete-tweet"; 6 | 7 | export const useDeleteTweet = () => { 8 | const queryClient = useQueryClient(); 9 | const pathname = usePathname(); 10 | const router = useRouter(); 11 | 12 | return useMutation({ 13 | mutationFn: ({ tweetId }: { tweetId: string }) => { 14 | if (pathname === `/status/${tweetId}`) { 15 | router.back(); 16 | } 17 | return deleteTweet(tweetId); 18 | }, 19 | onSuccess: () => { 20 | queryClient.invalidateQueries({ queryKey: ["tweets"] }); 21 | toast("Your tweet was deleted"); 22 | }, 23 | onError: (error) => { 24 | console.log(error); 25 | toast("Something went wrong"); 26 | }, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/tweets/hooks/use-like.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 | 3 | import { toggleLike } from "../api/toggle-like"; 4 | 5 | export const useLike = () => { 6 | const queryClient = useQueryClient(); 7 | 8 | return useMutation({ 9 | mutationFn: ({ 10 | tweetId, 11 | userId, 12 | }: { 13 | tweetId: string | undefined; 14 | userId: string; 15 | }) => { 16 | return toggleLike({ tweetId, userId }); 17 | }, 18 | onSuccess: () => { 19 | queryClient.invalidateQueries({ queryKey: ["tweets"] }); 20 | }, 21 | onError: () => { 22 | console.log("error"); 23 | }, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/features/tweets/hooks/use-pin-tweet.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 | 3 | import { pinTweet } from "../api/pin-tweet"; 4 | import { unpinTweet } from "../api/unpin-tweet"; 5 | 6 | export const usePinTweet = () => { 7 | const queryClient = useQueryClient(); 8 | 9 | return useMutation({ 10 | mutationFn: ({ 11 | tweetId, 12 | userId, 13 | action, 14 | }: { 15 | tweetId: string | undefined; 16 | userId: string; 17 | action: string; 18 | }) => { 19 | return action === "pin" ? pinTweet(tweetId, userId) : unpinTweet(userId); 20 | }, 21 | onSuccess: () => { 22 | queryClient.invalidateQueries({ queryKey: ["tweets"] }); 23 | }, 24 | onError: () => { 25 | console.log("error"); 26 | }, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/tweets/hooks/use-retweet.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 | 3 | import { handleRetweet } from "../api/handle-retweet"; 4 | 5 | export const useRetweet = (setIsModalOpen: (isModalOpen: boolean) => void) => { 6 | const QueryClient = useQueryClient(); 7 | 8 | return useMutation({ 9 | mutationFn: ({ tweetId, userId }: { tweetId: string; userId: string }) => { 10 | return handleRetweet(tweetId, userId); 11 | }, 12 | 13 | onMutate: () => { 14 | setIsModalOpen(false); 15 | }, 16 | 17 | onSuccess: () => { 18 | QueryClient.invalidateQueries({ queryKey: ["tweets"] }); 19 | }, 20 | 21 | onError: (error: any) => { 22 | console.log(error); 23 | }, 24 | 25 | onSettled: () => {}, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/features/tweets/hooks/use-tweet.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import getTweet from "../api/get-tweet"; 4 | import { ITweet } from "../types"; 5 | 6 | export const useTweet = ({ 7 | id, 8 | initialData, 9 | }: { 10 | id: string; 11 | initialData?: ITweet; 12 | }) => { 13 | return useQuery({ 14 | queryKey: ["tweets", id], 15 | queryFn: async () => { 16 | return getTweet(id); 17 | }, 18 | refetchOnWindowFocus: false, 19 | initialData: initialData ?? undefined, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/features/tweets/hooks/use-tweets.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useInfiniteQuery } from "@tanstack/react-query"; 3 | 4 | import { ITweet } from ".."; 5 | import { getTweets } from "../api/get-tweets"; 6 | 7 | interface IInfiniteTweets { 8 | nextId: string; 9 | tweets: ITweet[]; 10 | } 11 | 12 | export const useTweets = ({ 13 | queryKey = ["tweets"], 14 | type, 15 | id, 16 | }: { 17 | queryKey?: string[]; 18 | type?: string; 19 | id?: string; 20 | }) => { 21 | return useInfiniteQuery({ 22 | // eslint-disable-next-line @tanstack/query/exhaustive-deps 23 | queryKey, 24 | queryFn: ({ pageParam }) => { 25 | return getTweets({ 26 | pageParam, 27 | limit: 20, 28 | type, 29 | id, 30 | }); 31 | }, 32 | initialPageParam: "", 33 | 34 | getNextPageParam: (lastPage) => { 35 | return lastPage?.nextId; 36 | }, 37 | refetchOnWindowFocus: false, 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/features/tweets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/tweets"; 2 | export * from "./components/tweet"; 3 | export * from "./components/infinite-tweets"; 4 | export * from "./components/tweet-details"; 5 | export * from "./components/inspect-tweet-image-modal"; 6 | export * from "./components/options/tweet-options"; 7 | export * from "./components/quoted-tweet"; 8 | export * from "./components/tweet-quotes"; 9 | export * from "./components/tweet-media"; 10 | export * from "./hooks/use-tweets"; 11 | export * from "./hooks/use-tweet"; 12 | export * from "./types"; 13 | export * from "./api/get-tweet-metadata"; 14 | export * from "./components/comments"; 15 | export * from "./components/tweet-actions"; 16 | export * from "./components/tweet-author"; 17 | export * from "./components/tweet-creation-date"; 18 | export * from "./components/tweet-statistics"; 19 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-disable-body-scroll"; 2 | export * from "./use-debounce"; 3 | -------------------------------------------------------------------------------- /src/hooks/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ICookies { 2 | key: string; 3 | value?: string; 4 | method: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | export const useDebounce = (value: T, delay?: number): T => { 6 | const [debouncedValue, setDebouncedValue] = useState(value); 7 | 8 | useEffect(() => { 9 | const timer = setTimeout(() => setDebouncedValue(value), delay || 500); 10 | 11 | return () => { 12 | clearTimeout(timer); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | }; 18 | -------------------------------------------------------------------------------- /src/hooks/use-disable-body-scroll.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useCallback } from "react"; 3 | 4 | export const useDisableBodyScroll = () => { 5 | useCallback(() => { 6 | document.body.style.overflowY = "hidden"; 7 | document.body.style.paddingRight = "11px"; 8 | return () => { 9 | document.body.style.overflowY = "scroll"; 10 | document.body.style.paddingRight = "0px"; 11 | }; 12 | }, []); 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { NODE_ENV } from "@/config"; 4 | 5 | declare global { 6 | // allow global `var` declarations 7 | // eslint-disable-next-line no-var 8 | var prisma: PrismaClient | undefined; 9 | } 10 | 11 | export const prisma = 12 | global.prisma || 13 | new PrismaClient({ 14 | errorFormat: "minimal", 15 | log: [ 16 | { 17 | emit: "event", 18 | level: "query", 19 | }, 20 | ], 21 | }); 22 | 23 | if (NODE_ENV !== "production") global.prisma = prisma; 24 | -------------------------------------------------------------------------------- /src/lib/socket-io.ts: -------------------------------------------------------------------------------- 1 | import { io } from "socket.io-client"; 2 | 3 | import { SOCKET_URL } from "@/config"; 4 | 5 | const URL = SOCKET_URL; 6 | 7 | export const socket = io(URL, { 8 | autoConnect: false, 9 | transports: ["websocket"], 10 | reconnection: true, 11 | reconnectionAttempts: 10, 12 | reconnectionDelay: 1000, 13 | }); 14 | 15 | socket.on("connect_error", (error) => { 16 | console.log(`Connection Error: ${error.message}`); 17 | }); 18 | -------------------------------------------------------------------------------- /src/providers/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC } from "react"; 3 | 4 | import { ErrorBoundaryProvider } from "./error-boundary-provider"; 5 | import { NextAuthProvider } from "./next-auth-provider"; 6 | import { ReactQueryProvider } from "./react-query-provider"; 7 | 8 | type AppProvidersProps = { 9 | children: React.ReactNode; 10 | }; 11 | 12 | export const AppProviders: FC = ({ children }) => { 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/providers/next-auth-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SessionProvider } from "next-auth/react"; 4 | 5 | export const NextAuthProvider = ({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) => { 10 | return {children}; 11 | }; 12 | -------------------------------------------------------------------------------- /src/providers/react-query-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | import { useState } from "react"; 6 | 7 | export const ReactQueryProvider = ({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) => { 12 | const [queryClient] = useState( 13 | () => 14 | new QueryClient({ 15 | defaultOptions: { 16 | queries: { 17 | refetchOnWindowFocus: false, 18 | staleTime: 1000 * 5, 19 | gcTime: 1000 * 60 * 5, 20 | throwOnError: true, 21 | }, 22 | 23 | mutations: { 24 | throwOnError: true, 25 | }, 26 | }, 27 | }), 28 | ); 29 | 30 | return ( 31 | 32 | {children} 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/sass/abstracts/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./media-query"; 2 | @forward "./themes"; 3 | @forward "./z-index"; 4 | -------------------------------------------------------------------------------- /src/sass/abstracts/_media-query.scss: -------------------------------------------------------------------------------- 1 | $breakpoints: ( 2 | "small": 500px, 3 | "medium": 700px, 4 | "large": 1020px, 5 | "x-large": 1120px, 6 | "xx-large": 1300px, 7 | ); 8 | 9 | @mixin mq($key) { 10 | $size: map-get($breakpoints, $key); 11 | 12 | @media only screen and (min-width: $size) { 13 | @content; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/sass/abstracts/_z-index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --z-index-dropdown: 1000; 3 | --z-index-sticky: 1020; 4 | --z-index-fixed: 1030; 5 | --z-index-modal-backdrop: 1040; 6 | --z-index-modal: 1050; 7 | --z-index-popover: 1060; 8 | --z-index-tooltip: 1070; 9 | } 10 | -------------------------------------------------------------------------------- /src/sass/base/_base.scss: -------------------------------------------------------------------------------- 1 | @use "./typography" as *; 2 | 3 | html, 4 | body { 5 | font-family: var(--ff-base); 6 | } 7 | -------------------------------------------------------------------------------- /src/sass/base/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./reset"; 2 | @forward "./typography"; 3 | @forward "./base"; 4 | -------------------------------------------------------------------------------- /src/sass/base/_reset.scss: -------------------------------------------------------------------------------- 1 | *:where(:not(iframe, canvas, img, svg, video):not(svg *)) { 2 | all: unset; 3 | display: revert; 4 | } 5 | 6 | /* Preferred box-sizing value */ 7 | *, 8 | *::before, 9 | *::after { 10 | box-sizing: border-box; 11 | } 12 | 13 | /* 14 | Remove list styles (bullets/numbers) 15 | in case you use it with normalize.css 16 | */ 17 | ol, 18 | ul { 19 | list-style: none; 20 | } 21 | 22 | /* For images to not be able to exceed their container */ 23 | img { 24 | max-width: 100%; 25 | } 26 | 27 | /* Removes spacing between cells in tables */ 28 | table { 29 | border-collapse: collapse; 30 | } 31 | 32 | /* Revert the 'white-space' property for textarea elements on Safari */ 33 | textarea { 34 | white-space: revert; 35 | } 36 | 37 | button { 38 | display: block; 39 | } 40 | -------------------------------------------------------------------------------- /src/sass/main.scss: -------------------------------------------------------------------------------- 1 | @use "./abstracts"; 2 | @use "./base"; 3 | -------------------------------------------------------------------------------- /src/stores/use-auth-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface IModal { 4 | isUserModalOpen: boolean; 5 | isSignOutModalOpen: boolean; 6 | openUserModal: () => void; 7 | closeUserModal: () => void; 8 | openSignOutModal: () => void; 9 | closeSignOutModal: () => void; 10 | } 11 | 12 | export const useAuthModal = create((set) => ({ 13 | isUserModalOpen: false, 14 | isSignOutModalOpen: false, 15 | openUserModal: () => set({ isUserModalOpen: true }), 16 | closeUserModal: () => set({ isUserModalOpen: false }), 17 | openSignOutModal: () => set({ isSignOutModalOpen: true }), 18 | closeSignOutModal: () => set({ isSignOutModalOpen: false }), 19 | })); 20 | -------------------------------------------------------------------------------- /src/stores/use-hamburger.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface IHamburger { 4 | isHamburgerOpen: boolean; 5 | openHamburger: () => void; 6 | closeHamburger: () => void; 7 | } 8 | 9 | export const useHamburger = create((set) => ({ 10 | isHamburgerOpen: false, 11 | openHamburger: () => set({ isHamburgerOpen: true }), 12 | closeHamburger: () => set({ isHamburgerOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /src/stores/use-tweet-statistics.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | import { IUser } from "@/features/profile"; 4 | 5 | interface ITweetStatistics { 6 | isTweetStatisticsModalOpen: boolean; 7 | openTweetStatisticsModal: () => void; 8 | closeTweetStatisticsModal: () => void; 9 | authors: IUser[] | null; 10 | setAuthors: (authors: IUser[]) => void; 11 | statisticType: string | null; 12 | setStatisticType: (statisticType: string) => void; 13 | } 14 | 15 | export const useTweetStatistics = create((set) => ({ 16 | isTweetStatisticsModalOpen: false, 17 | openTweetStatisticsModal: () => 18 | set({ isTweetStatisticsModalOpen: true, authors: null }), 19 | closeTweetStatisticsModal: () => set({ isTweetStatisticsModalOpen: false }), 20 | authors: null, 21 | 22 | setAuthors: (authors) => set({ authors }), 23 | statisticType: null, 24 | setStatisticType: (statisticType: string) => set({ statisticType }), 25 | })); 26 | -------------------------------------------------------------------------------- /src/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from "@prisma/client"; 2 | import "next-auth/jwt"; 3 | 4 | declare module "next-auth/jwt" { 5 | interface JWT { 6 | id: string; 7 | role: UserRole; 8 | } 9 | } 10 | declare module "next-auth" { 11 | interface Session { 12 | user: { 13 | id: string; 14 | role: UserRole; 15 | } & DefaultSession["user"]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export const cn = (...inputs: ClassValue[]) => { 5 | return twMerge(clsx(inputs)); 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/supabase-client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | 3 | import { SUPABASE_URL, SUPABASE_ANON_KEY } from "@/config"; 4 | 5 | export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { 6 | auth: { 7 | persistSession: false, 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------