├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── task.md │ ├── usabilty_report.md │ └── user_story.md ├── PULL_REQUEST_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE │ ├── generic-pr.md │ └── post_mortem.md └── workflows │ ├── build_server.yml │ ├── clean_caches.yml │ ├── generate.yml │ ├── playwright.yml │ └── prettier.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .vercelignore ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docker-compose-dev.yml ├── docker-compose.prod.yml ├── docker-compose.router.yml ├── docker-compose.staging.yml ├── docker-compose ├── build-local.sh ├── create-secrets.sh └── readme.md ├── funding.json ├── mongo-init └── init.js ├── package.json ├── packages ├── app │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc.json │ ├── Dockerfile.dev │ ├── app │ │ ├── (legal) │ │ │ ├── data-request │ │ │ │ ├── components │ │ │ │ │ └── createRequest.tsx │ │ │ │ └── page.tsx │ │ │ ├── privacy │ │ │ │ └── page.tsx │ │ │ └── terms │ │ │ │ └── page.tsx │ │ ├── (vod) │ │ │ ├── archive │ │ │ │ └── page.tsx │ │ │ └── watch │ │ │ │ └── page.tsx │ │ ├── [organization] │ │ │ ├── [event] │ │ │ │ ├── components │ │ │ │ │ └── EventHomeComponent.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── schedule │ │ │ │ │ ├── components │ │ │ │ │ │ ├── DateSelect.tsx │ │ │ │ │ │ ├── ScheduleCard.tsx │ │ │ │ │ │ ├── ScheduleCardModal.tsx │ │ │ │ │ │ ├── ScheduleComponent.tsx │ │ │ │ │ │ └── StageSelect.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── speakers │ │ │ │ │ ├── components │ │ │ │ │ │ ├── SpeakerCard.tsx │ │ │ │ │ │ ├── SpeakerComponent.tsx │ │ │ │ │ │ ├── SpeakerModal.tsx │ │ │ │ │ │ └── SpeakerPhoto.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── stage │ │ │ │ │ └── components │ │ │ │ │ ├── StageComponent.tsx │ │ │ │ │ ├── StagePreview.tsx │ │ │ │ │ └── UpcomingSession.tsx │ │ │ ├── components │ │ │ │ ├── ChannelBanner.tsx │ │ │ │ ├── ChannelDescription.tsx │ │ │ │ ├── ChannelPlayer.tsx │ │ │ │ ├── ChannelShareIcons.tsx │ │ │ │ ├── ShareVideoMenuItem.tsx │ │ │ │ ├── UpcomingStreams.tsx │ │ │ │ ├── VideoDownload.tsx │ │ │ │ ├── ViewCounts.tsx │ │ │ │ └── WatchGrid.tsx │ │ │ ├── layout.tsx │ │ │ ├── livestream │ │ │ │ ├── components │ │ │ │ │ ├── ArchiveVideosSkeleton.tsx │ │ │ │ │ ├── CalendarReminder.tsx │ │ │ │ │ ├── Counter.tsx │ │ │ │ │ ├── PaginationSkeleteon.tsx │ │ │ │ │ └── Player.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ ├── playlists │ │ │ │ ├── [playlistId] │ │ │ │ │ └── page.tsx │ │ │ │ └── components │ │ │ │ │ ├── ArchivePlaylists.tsx │ │ │ │ │ ├── OrganizationVideos.tsx │ │ │ │ │ ├── PlaylistCard.tsx │ │ │ │ │ ├── PlaylistShareDialog.tsx │ │ │ │ │ ├── PlaylistVideo.tsx │ │ │ │ │ ├── PlaylistVideoPlayer.tsx │ │ │ │ │ ├── PlaylistVideoPlayerSkeleton.tsx │ │ │ │ │ └── PlaylistVideos.tsx │ │ │ ├── videos │ │ │ │ ├── components │ │ │ │ │ ├── ArchiveVideos.tsx │ │ │ │ │ └── eventSelect.tsx │ │ │ │ └── page.tsx │ │ │ └── watch │ │ │ │ └── page.tsx │ │ ├── api │ │ │ ├── auth │ │ │ │ └── [...nextauth] │ │ │ │ │ └── route.ts │ │ │ ├── google │ │ │ │ ├── callback │ │ │ │ │ └── route.ts │ │ │ │ └── request │ │ │ │ │ └── route.ts │ │ │ ├── twitter │ │ │ │ ├── callback │ │ │ │ │ └── route.ts │ │ │ │ └── request │ │ │ │ │ └── route.ts │ │ │ └── video-download │ │ │ │ └── route.ts │ │ ├── auth │ │ │ ├── auth-error │ │ │ │ └── page.tsx │ │ │ ├── auth-success │ │ │ │ └── page.tsx │ │ │ ├── login │ │ │ │ ├── components │ │ │ │ │ └── SignInWithSocials.tsx │ │ │ │ └── page.tsx │ │ │ ├── logout │ │ │ │ └── page.tsx │ │ │ └── magic-link │ │ │ │ └── page.tsx │ │ ├── embed │ │ │ └── page.tsx │ │ ├── error.tsx │ │ ├── explore │ │ │ ├── components │ │ │ │ ├── ExploreTabs.tsx │ │ │ │ └── FeaturedEvents.tsx │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ ├── redirect │ │ │ └── google │ │ │ │ └── page.tsx │ │ ├── speaker │ │ │ └── [session] │ │ │ │ ├── components │ │ │ │ ├── ClientSidePlayer.tsx │ │ │ │ ├── SpeakerYoutubePublishButton.tsx │ │ │ │ └── ZoraUploadButton.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ └── studio │ │ │ ├── (home) │ │ │ ├── components │ │ │ │ ├── CreateOrganizationForm.tsx │ │ │ │ └── JoinOrganizationForm.tsx │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ │ ├── [organization] │ │ │ ├── (no-side-bar) │ │ │ │ ├── clips │ │ │ │ │ ├── [stageId] │ │ │ │ │ │ ├── ClipPageContext.tsx │ │ │ │ │ │ ├── Controls │ │ │ │ │ │ │ ├── KeyboardShortcuts.tsx │ │ │ │ │ │ │ ├── ZoomControls.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Player.tsx │ │ │ │ │ │ ├── Timeline │ │ │ │ │ │ │ ├── PlayHead.tsx │ │ │ │ │ │ │ ├── TimelineContext.tsx │ │ │ │ │ │ │ ├── TrimmControls.tsx │ │ │ │ │ │ │ ├── TrimmControlsContext.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── useTimeline.ts │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ ├── sidebar │ │ │ │ │ │ │ ├── Transcipts │ │ │ │ │ │ │ │ ├── TranscriptText.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── clips │ │ │ │ │ │ │ │ ├── Clip.tsx │ │ │ │ │ │ │ │ ├── ClipsContext.tsx │ │ │ │ │ │ │ │ ├── CreateClipForm.tsx │ │ │ │ │ │ │ │ ├── Preview.tsx │ │ │ │ │ │ │ │ ├── TimeSetter.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── markers │ │ │ │ │ │ │ │ ├── AddOrEditMarkerForm.tsx │ │ │ │ │ │ │ │ ├── DeleteMarkerButton.tsx │ │ │ │ │ │ │ │ ├── ExtractHighlightsForm.tsx │ │ │ │ │ │ │ │ ├── ImportMarkersForm.tsx │ │ │ │ │ │ │ │ ├── Marker.tsx │ │ │ │ │ │ │ │ ├── TimelineMarker.tsx │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ └── markersContext.tsx │ │ │ │ │ │ └── topBar │ │ │ │ │ │ │ ├── SelectAnimation.tsx │ │ │ │ │ │ │ ├── SessionSelector.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── (root) │ │ │ │ ├── destinations │ │ │ │ │ ├── components │ │ │ │ │ │ ├── AddDestination.tsx │ │ │ │ │ │ ├── DeleteDestination.tsx │ │ │ │ │ │ ├── TwitterConnectButton.tsx │ │ │ │ │ │ └── YoutubeConnectButton.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── library │ │ │ │ │ ├── [session] │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── AddSpeakersInput.tsx │ │ │ │ │ │ │ ├── EditSessionForm.tsx │ │ │ │ │ │ │ ├── ImageDropzone.tsx │ │ │ │ │ │ │ ├── SessionAccordion.tsx │ │ │ │ │ │ │ ├── SessionOptions.tsx │ │ │ │ │ │ │ ├── SessionTranscriptions.tsx │ │ │ │ │ │ │ ├── UploadToYoutubeButton.tsx │ │ │ │ │ │ │ ├── UploadTwitterButton.tsx │ │ │ │ │ │ │ └── form │ │ │ │ │ │ │ │ ├── BasicFormFields.tsx │ │ │ │ │ │ │ │ ├── FormActions.tsx │ │ │ │ │ │ │ │ ├── ThumbnailSection.tsx │ │ │ │ │ │ │ │ ├── VisibilitySelector.tsx │ │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── components │ │ │ │ │ │ ├── AddToPlaylistMenu.tsx │ │ │ │ │ │ ├── ClipContentButton.tsx │ │ │ │ │ │ ├── CreatePlaylistDialog.tsx │ │ │ │ │ │ ├── DeleteAsset.tsx │ │ │ │ │ │ ├── DropdownActions.tsx │ │ │ │ │ │ ├── EmptyLibrary.tsx │ │ │ │ │ │ ├── GetHashButton.tsx │ │ │ │ │ │ ├── InjectUrlInput.tsx │ │ │ │ │ │ ├── LayoutSelection.tsx │ │ │ │ │ │ ├── Library.tsx │ │ │ │ │ │ ├── LibraryFilter.tsx │ │ │ │ │ │ ├── Pagination.tsx │ │ │ │ │ │ ├── PlaylistTable.tsx │ │ │ │ │ │ ├── TableCells.tsx │ │ │ │ │ │ ├── UploadVideoDialog.tsx │ │ │ │ │ │ ├── VisibilityButton.tsx │ │ │ │ │ │ ├── misc │ │ │ │ │ │ │ ├── PopoverActions.tsx │ │ │ │ │ │ │ └── VideoCardWithMenu.tsx │ │ │ │ │ │ ├── tabs │ │ │ │ │ │ │ ├── PlaylistsTab.tsx │ │ │ │ │ │ │ └── VideosTab.tsx │ │ │ │ │ │ └── upload │ │ │ │ │ │ │ ├── Dropzone.tsx │ │ │ │ │ │ │ ├── UploadProgress.tsx │ │ │ │ │ │ │ └── UploadVideoForm.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── playlists │ │ │ │ │ │ └── [playlistId] │ │ │ │ │ │ ├── components │ │ │ │ │ │ ├── PlaylistDetail.tsx │ │ │ │ │ │ ├── PlaylistDropdownActions.tsx │ │ │ │ │ │ └── PlaylistTableCells.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── livestreams │ │ │ │ │ ├── [streamId] │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── ClientSidePlayer.tsx │ │ │ │ │ │ │ ├── ClipsList.tsx │ │ │ │ │ │ │ ├── DeleteMultistream.tsx │ │ │ │ │ │ │ ├── Destinations.tsx │ │ │ │ │ │ │ ├── LivestreamEmbedCode.tsx │ │ │ │ │ │ │ ├── Multistream.tsx │ │ │ │ │ │ │ ├── PublishLivestream.tsx │ │ │ │ │ │ │ ├── ShareAndEmbed.tsx │ │ │ │ │ │ │ ├── Sidebar.tsx │ │ │ │ │ │ │ ├── StageControls.tsx │ │ │ │ │ │ │ ├── StageDataImport │ │ │ │ │ │ │ │ ├── ImportDataButton.tsx │ │ │ │ │ │ │ │ ├── ImportPreviewDialog.tsx │ │ │ │ │ │ │ │ └── ViewMarkersDialog.tsx │ │ │ │ │ │ │ ├── StreamConfigWithPlayer.tsx │ │ │ │ │ │ │ ├── StreamHeader.tsx │ │ │ │ │ │ │ ├── StreamHealth.tsx │ │ │ │ │ │ │ └── StreamPlatforms │ │ │ │ │ │ │ │ ├── CreateMultistreamTarget.tsx │ │ │ │ │ │ │ │ ├── CreateYoutubeStream.tsx │ │ │ │ │ │ │ │ ├── StreamPlatforms.tsx │ │ │ │ │ │ │ │ └── forms │ │ │ │ │ │ │ │ └── CustomRtmpForm.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── components │ │ │ │ │ │ ├── CreateLivestreamModal.tsx │ │ │ │ │ │ ├── CreateLivestreamOptions.tsx │ │ │ │ │ │ ├── DeleteLivestream.tsx │ │ │ │ │ │ ├── EditLivestream.tsx │ │ │ │ │ │ ├── ImportSchedule.tsx │ │ │ │ │ │ ├── LivestreamActions.tsx │ │ │ │ │ │ ├── LivestreamTable.tsx │ │ │ │ │ │ ├── LivestreamTableCard.tsx │ │ │ │ │ │ ├── ShareLivestream.tsx │ │ │ │ │ │ ├── SubscriptionAlert.tsx │ │ │ │ │ │ └── ToggleLivestreamVisibility.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── payments │ │ │ │ │ ├── components │ │ │ │ │ │ ├── ActiveSubscriptionView.tsx │ │ │ │ │ │ ├── ExpiredSubscriptionCard.tsx │ │ │ │ │ │ ├── ExpiredSubscriptionView.tsx │ │ │ │ │ │ ├── LoadingState.tsx │ │ │ │ │ │ ├── MonthlySubscriptionTiers.tsx │ │ │ │ │ │ ├── NewSubscriptionView.tsx │ │ │ │ │ │ ├── PricingTiers.tsx │ │ │ │ │ │ └── ResourceCounter.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── settings │ │ │ │ │ ├── components │ │ │ │ │ │ └── NavigationItem.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── team │ │ │ │ │ ├── components │ │ │ │ │ ├── DeleteTeamMember.tsx │ │ │ │ │ └── InviteCode.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── nfts │ │ │ │ ├── [nftId] │ │ │ │ ├── components │ │ │ │ │ └── ContractDetails.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── components │ │ │ │ ├── CreateNFTCollectionModal.tsx │ │ │ │ ├── MintNftSort.tsx │ │ │ │ └── NFTCollectionCard.tsx │ │ │ │ ├── create │ │ │ │ ├── components │ │ │ │ │ ├── AddMedia.tsx │ │ │ │ │ ├── AddMediaTabItem.tsx │ │ │ │ │ ├── CollectionDetails.tsx │ │ │ │ │ ├── CreateNFTForm.tsx │ │ │ │ │ ├── CreateNFTModal.tsx │ │ │ │ │ ├── PublishingNFTModal.tsx │ │ │ │ │ ├── SelectedMediaItem.tsx │ │ │ │ │ ├── ShareCollection.tsx │ │ │ │ │ ├── ShareVideoNFT.tsx │ │ │ │ │ └── TransactionHash.tsx │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ ├── auth.ts │ ├── components.json │ ├── components │ │ ├── Layout │ │ │ ├── EmbedLayout.tsx │ │ │ ├── Footer.tsx │ │ │ ├── HomePageNavbar.tsx │ │ │ ├── Navbar.tsx │ │ │ ├── NavbarStudio.tsx │ │ │ ├── NavbarTop.tsx │ │ │ ├── Navigation.tsx │ │ │ ├── NavigationItem.tsx │ │ │ └── SwitchOrganization.tsx │ │ ├── Sidebar │ │ │ ├── Sidebar.tsx │ │ │ └── SidebarMenu.tsx │ │ ├── misc │ │ │ ├── ConnectWalletButton.tsx │ │ │ ├── CopyString.tsx │ │ │ ├── CopyText.tsx │ │ │ ├── MarkdownDisplay.tsx │ │ │ ├── SearchBar │ │ │ │ ├── SearchResults.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── useSearch.ts │ │ │ ├── Support │ │ │ │ ├── SupportForm.tsx │ │ │ │ └── index.tsx │ │ │ ├── Table │ │ │ │ └── TableSkeleton.tsx │ │ │ ├── TableSort.tsx │ │ │ ├── UserDropdown.tsx │ │ │ ├── UserProfile.tsx │ │ │ ├── VideoCard │ │ │ │ ├── LivestreamCard.tsx │ │ │ │ ├── VideoCardProcessing.tsx │ │ │ │ ├── VideoCardSkeleton.tsx │ │ │ │ ├── VideoCardWithMenu.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── thumbnail.tsx │ │ │ ├── VideoDownloadClient.tsx │ │ │ ├── Videos.tsx │ │ │ ├── form │ │ │ │ ├── colorPicker.tsx │ │ │ │ ├── dataConfigElement.tsx │ │ │ │ ├── datePicker.tsx │ │ │ │ ├── imageUpload.tsx │ │ │ │ ├── timePicker.tsx │ │ │ │ └── videoUpload.tsx │ │ │ └── interact │ │ │ │ ├── Alert.tsx │ │ │ │ ├── EmbedButton.tsx │ │ │ │ ├── InfoHoverCard.tsx │ │ │ │ └── ShareButton.tsx │ │ ├── plugins │ │ │ ├── NFT │ │ │ │ └── index.tsx │ │ │ ├── NFTMintComponent.tsx │ │ │ └── SignUp │ │ │ │ └── index.tsx │ │ ├── sessions │ │ │ ├── CollectVideButton.tsx │ │ │ ├── DropdownMenuWithActionButtons.tsx │ │ │ ├── InfoBoxDescription.tsx │ │ │ ├── LiveIndicator.tsx │ │ │ ├── SessionInfoBox.tsx │ │ │ ├── SessionList.tsx │ │ │ └── TranscriptionModal.tsx │ │ ├── speakers │ │ │ └── speakerIcon.tsx │ │ └── ui │ │ │ ├── Player.tsx │ │ │ ├── TitleTextItem.tsx │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── combo-box.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── crezenda.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── feature-button.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── resizable.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── text-placeholder.tsx │ │ │ ├── textarea.tsx │ │ │ └── tooltip.tsx │ ├── lib │ │ ├── actions │ │ │ ├── auth.ts │ │ │ ├── chat.ts │ │ │ ├── events.ts │ │ │ ├── imageUpload.ts │ │ │ ├── livepeer.ts │ │ │ ├── marker.ts │ │ │ ├── nftCollection.ts │ │ │ ├── organizations.ts │ │ │ ├── playlists.ts │ │ │ ├── sessions.ts │ │ │ ├── stages.ts │ │ │ ├── state.ts │ │ │ ├── support.ts │ │ │ └── users.ts │ │ ├── context │ │ │ ├── GeneralContext.tsx │ │ │ ├── LoadingContext.tsx │ │ │ ├── MobileContext.tsx │ │ │ ├── OrganizationContext.tsx │ │ │ ├── SiweContext.tsx │ │ │ ├── TopNavbarContext.tsx │ │ │ └── UserContext.tsx │ │ ├── contract │ │ │ └── index.ts │ │ ├── data.ts │ │ ├── hooks │ │ │ ├── useClickOutside.ts │ │ │ ├── useCreateClip.ts │ │ │ ├── useDebounce.ts │ │ │ ├── useGenerateThumbnail.ts │ │ │ ├── useLocalStorage.tsx │ │ │ ├── useMediaQuery.ts │ │ │ ├── useOrganization.ts │ │ │ ├── usePayment.ts │ │ │ ├── usePlayer.ts │ │ │ └── useSearchParams.ts │ │ ├── schema.ts │ │ ├── services │ │ │ ├── authService.ts │ │ │ ├── chatService.ts │ │ │ ├── eventService.tsx │ │ │ ├── fetch-client.ts │ │ │ ├── googleDriveService.ts │ │ │ ├── googleSheetService.ts │ │ │ ├── imageUploadService.ts │ │ │ ├── markerSevice.ts │ │ │ ├── nftCollectionService.ts │ │ │ ├── organizationService.tsx │ │ │ ├── playlistService.ts │ │ │ ├── sessionService.ts │ │ │ ├── stageService.tsx │ │ │ ├── stateService.ts │ │ │ ├── stripeService.ts │ │ │ ├── supportService.tsx │ │ │ ├── userService.ts │ │ │ └── videoUploadService.ts │ │ ├── svg │ │ │ ├── DefaultThumbnail.tsx │ │ │ ├── EmptyFolder.tsx │ │ │ ├── StreamethLogo.tsx │ │ │ ├── StreamethLogoGray.tsx │ │ │ ├── StreamethLogoWhite.tsx │ │ │ ├── StreamethStudio.tsx │ │ │ ├── UploadComplete.tsx │ │ │ └── YoutubeIcon.tsx │ │ ├── types.ts │ │ ├── uploadVideo.ts │ │ └── utils │ │ │ ├── generateGoogleCalendar.ts │ │ │ ├── googleAuth.ts │ │ │ ├── metadata.ts │ │ │ ├── resizeImage.ts │ │ │ ├── subscription.ts │ │ │ ├── time.tsx │ │ │ ├── timezone.tsx │ │ │ ├── twitterAuth.ts │ │ │ └── utils.ts │ ├── middleware.ts │ ├── next.config.js │ ├── not-found.tsx │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── Base-house.jpg │ │ ├── Folder.png │ │ ├── UserEmptyIcon.png │ │ ├── backgrounds │ │ │ ├── livestreamBg.png │ │ │ ├── nftBg.jpg │ │ │ └── nftBg.svg │ │ ├── cover.png │ │ ├── farcaster-transparent-white.png │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── NFTSuccess.png │ │ │ ├── empty-box.png │ │ │ ├── multipleNFT.png │ │ │ ├── singleNFT.png │ │ │ ├── videoPlaceholder.png │ │ │ ├── youtube-tutorial.png │ │ │ └── youtube_social_icon_red.png │ │ ├── legal │ │ │ ├── privacy.md │ │ │ └── terms.md │ │ ├── lens-green.png │ │ ├── lib_wsURRdlMczHRgWBe │ │ │ ├── 002clfqcrzy725fn.png │ │ │ ├── 0tcvaer9b0dwp6gg.png │ │ │ ├── 0ud783pd9etcohgd.png │ │ │ ├── 26sfvru7y3df4lpu.png │ │ │ ├── 2hesu5v0s2y9a7h5.png │ │ │ ├── 329bez6yoeajgob0.png │ │ │ ├── 4179bdhw7ckua4hb.png │ │ │ ├── 46j3ppjs675nuhka.png │ │ │ ├── 4k1ngp6h5n5l1u5l.jpg │ │ │ ├── 4xmsxtzufoulc5af.png │ │ │ ├── 550nopa7lubbte09.jpg │ │ │ ├── 7hf72jf59l4e0mh9.png │ │ │ ├── 7uegl4bozj0pbp5x.png │ │ │ ├── 83fie8t0fjm8is86.png │ │ │ ├── 90fnji3oxuxld4l5.png │ │ │ ├── 9upukn8zuxn9rewm.png │ │ │ ├── a17fxhoo1yzgkowd.jpg │ │ │ ├── a69ad1z1iuc16c6j.png │ │ │ ├── a9c854il4fopdsp0.png │ │ │ ├── byv0vxn9ncxss1tj.png │ │ │ ├── ccz1ztuk5ny0qloe.png │ │ │ ├── cebcodsnth35kyyg.png │ │ │ ├── d8emwluemsveqjxh.png │ │ │ ├── defjaol1kkd8qw0p.png │ │ │ ├── ee54i24u1no8ee51.png │ │ │ ├── fxwq49em5sfw8gbu.png │ │ │ ├── gv2jrclmcqnmpc3y.png │ │ │ ├── hf1359h01lj90puc.jpg │ │ │ ├── hnw3w49fub3pfo8b.png │ │ │ ├── igfide8bqmwmyoha.png │ │ │ ├── kfql38xo5bq7u5f3.png │ │ │ ├── ki5ehzpo5d6umljx.png │ │ │ ├── lepklk4j8usqgbnu.png │ │ │ ├── lm5yh6zo4xfktsd4.png │ │ │ ├── n7lvh3tpq34a2b6c.png │ │ │ ├── oda6gbylddkx0sx4.jpg │ │ │ ├── pgm18l3l2za5a1nl.jpg │ │ │ ├── ppz6updxo1mb5m1s.png │ │ │ ├── r1t65rm26qvise2c.png │ │ │ ├── r2kkux1myqgch11c.jpg │ │ │ ├── rtmq0dmhuzf7k5fn.png │ │ │ ├── sa6qfea8vo2smjyp.png │ │ │ ├── sm5tz6r6vwjsy3fn.png │ │ │ ├── sstwil0urefp64f5.png │ │ │ ├── t8254hkl9zfudy6e.png │ │ │ ├── u4bku1qbk3x61o2z.png │ │ │ ├── wrgo0xbslsvxx7ho.jpg │ │ │ ├── xhjg9gn5lpi674e6.png │ │ │ └── zon4mxn9vxoqd8r2.jpg │ │ ├── livepeer-logo.png │ │ ├── livepeer.svg │ │ ├── livepeer_black.png │ │ ├── login-background.png │ │ ├── logo.png │ │ ├── logo_dark.png │ │ ├── streameth_banner.png │ │ ├── streameth_twitter_banner.jpeg │ │ ├── studio_logo.png │ │ ├── success.png │ │ └── twitter-white.png │ ├── tailwind.config.js │ └── tsconfig.json ├── contracts │ ├── .gitignore │ ├── README.md │ ├── contracts │ │ ├── VIdeoNFTFactory.sol │ │ └── VideoNFT.sol │ ├── hardhat.config.ts │ ├── package.json │ ├── scripts │ │ └── deploy.ts │ └── tsconfig.json ├── reel-creator │ ├── .dockerignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmrc │ ├── Dockerfile │ ├── Dockerfile.dev │ ├── README.md │ ├── app │ │ ├── api │ │ │ └── lambda │ │ │ │ ├── progress │ │ │ │ └── route.ts │ │ │ │ └── render │ │ │ │ └── route.ts │ │ ├── data │ │ │ └── transcription.js │ │ ├── favicon.ico │ │ ├── hooks │ │ │ └── useCurrentPlayerFrame.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components.json │ ├── components │ │ ├── AlignEnd.tsx │ │ ├── Button.tsx │ │ ├── Container.tsx │ │ ├── DownloadButton.tsx │ │ ├── EditorSidebar.tsx │ │ ├── Error.tsx │ │ ├── Input.tsx │ │ ├── Labels.tsx │ │ ├── Modal.tsx │ │ ├── Player.tsx │ │ ├── PlayerControl.tsx │ │ ├── ProgressBar.tsx │ │ ├── RenderControls.tsx │ │ ├── Spacing.tsx │ │ ├── Spinner.tsx │ │ ├── Tips.tsx │ │ ├── TranscriptSelector.tsx │ │ ├── VideoResizer.tsx │ │ ├── sidebar-components │ │ │ ├── AnimationSettings.tsx │ │ │ ├── BrandingSettings.tsx │ │ │ ├── CaptionSettings.tsx │ │ │ └── LayoutSettings.tsx │ │ ├── timeline │ │ │ ├── TimelineEvents.tsx │ │ │ ├── TimelineMarkers.tsx │ │ │ ├── TimelinePlayhead.tsx │ │ │ ├── TimelineToolbar.tsx │ │ │ └── index.tsx │ │ └── ui │ │ │ ├── accordion.tsx │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── dialog.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── select.tsx │ │ │ ├── slider.tsx │ │ │ └── switch.tsx │ ├── config.mjs │ ├── context │ │ ├── EditorContext.tsx │ │ └── TimelineContext.tsx │ ├── deploy.mjs │ ├── helpers │ │ ├── api-response.ts │ │ └── use-rendering.ts │ ├── lambda │ │ └── api.ts │ ├── lib │ │ └── utils.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── remotion.config.ts │ ├── remotion │ │ ├── Editor │ │ │ ├── Captions.tsx │ │ │ ├── Editor.tsx │ │ │ ├── constants.tsx │ │ │ └── index.tsx │ │ ├── Root.tsx │ │ ├── index.ts │ │ └── webpack-override.mjs │ ├── styles │ │ └── global.css │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── types │ │ ├── constants.ts │ │ ├── fluent-ffmpeg.d.ts │ │ └── schema.ts │ ├── vercel.json │ └── yarn.lock ├── server │ ├── .gitignore │ ├── .mocharc.json │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc.json │ ├── README.md │ ├── package.json │ ├── src │ │ ├── Dockerfile │ │ ├── Dockerfile.dev │ │ ├── app.ts │ │ ├── config │ │ │ └── index.ts │ │ ├── controllers │ │ │ ├── auth.controller.ts │ │ │ ├── chat.controller.ts │ │ │ ├── event.controller.ts │ │ │ ├── index.controller.ts │ │ │ ├── marker.controller.ts │ │ │ ├── nft.controller.ts │ │ │ ├── organization.controller.ts │ │ │ ├── playlist.controller.ts │ │ │ ├── schedule-import.controller.ts │ │ │ ├── session.controller.ts │ │ │ ├── speaker.controller.ts │ │ │ ├── stage.controller.ts │ │ │ ├── state.controller.ts │ │ │ ├── stream.controller.ts │ │ │ ├── stripe.controller.ts │ │ │ ├── support.controller.ts │ │ │ └── user.controller.ts │ │ ├── databases │ │ │ ├── index.ts │ │ │ └── storage │ │ │ │ ├── db.ts │ │ │ │ ├── fs.ts │ │ │ │ └── index.ts │ │ ├── dtos │ │ │ ├── auth │ │ │ │ └── auth.dto.ts │ │ │ ├── chat │ │ │ │ └── create-chat.dto.ts │ │ │ ├── event │ │ │ │ ├── create-event.dto.ts │ │ │ │ └── update-event.dto.ts │ │ │ ├── marker │ │ │ │ ├── create-marker.dto.ts │ │ │ │ └── update-marker.dto.ts │ │ │ ├── nft │ │ │ │ ├── create-colletion.dto.ts │ │ │ │ └── update-collection.dto.ts │ │ │ ├── organization │ │ │ │ ├── create-organization.dto.ts │ │ │ │ ├── orgid.dto.ts │ │ │ │ └── update-organization.dto.ts │ │ │ ├── playlist │ │ │ │ ├── create-playlist.dto.ts │ │ │ │ └── update-playlist.dto.ts │ │ │ ├── schedule-importer │ │ │ │ └── create-import.dto.ts │ │ │ ├── session │ │ │ │ ├── create-session.dto.ts │ │ │ │ ├── update-session.dto.ts │ │ │ │ └── upload-session.dto.ts │ │ │ ├── speaker │ │ │ │ └── create-speaker.dto.ts │ │ │ ├── stage │ │ │ │ ├── create-hls.dto.ts │ │ │ │ ├── create-stage.dto.ts │ │ │ │ ├── livestream.dto.ts │ │ │ │ └── update-stage.dto.ts │ │ │ ├── state │ │ │ │ ├── create-state.dto.ts │ │ │ │ └── update-state.dto.ts │ │ │ ├── stream │ │ │ │ ├── create-clip.dto.ts │ │ │ │ ├── create-multistream.dto.ts │ │ │ │ └── delete-multistream.dto.ts │ │ │ ├── stripe │ │ │ │ ├── create-checkout-session.dto.ts │ │ │ │ └── create-portal-session.dto.ts │ │ │ ├── support │ │ │ │ └── create-ticket.dto.ts │ │ │ └── user │ │ │ │ └── user.dto.ts │ │ ├── exceptions │ │ │ └── HttpException.ts │ │ ├── http │ │ │ ├── auth.http │ │ │ └── users.http │ │ ├── interfaces │ │ │ ├── auth.interface.ts │ │ │ ├── chat.interface.ts │ │ │ ├── clip.editor.interface.ts │ │ │ ├── clip.interface.ts │ │ │ ├── event.interface.ts │ │ │ ├── livepeer.interface.ts │ │ │ ├── livepeer.webhook.interface.ts │ │ │ ├── marker.interface.ts │ │ │ ├── nft.collection.interface.ts │ │ │ ├── organization.interface.ts │ │ │ ├── playlist.interface.ts │ │ │ ├── remotion.webhook.interface.ts │ │ │ ├── schedule-importer.interface.ts │ │ │ ├── session.interface.ts │ │ │ ├── speaker.interface.ts │ │ │ ├── stage.interface.ts │ │ │ ├── state.interface.ts │ │ │ ├── storage.interface.ts │ │ │ ├── stream.interface.ts │ │ │ ├── support.interface.ts │ │ │ ├── upload.session.interface.ts │ │ │ └── user.interface.ts │ │ ├── middlewares │ │ │ ├── auth.middleware.ts │ │ │ ├── error.middleware.ts │ │ │ └── multer.middleware.ts │ │ ├── models │ │ │ ├── chat.model.ts │ │ │ ├── clip.editor.model.ts │ │ │ ├── event.model.ts │ │ │ ├── markers.model.ts │ │ │ ├── nft.collection.model.ts │ │ │ ├── organization.model.ts │ │ │ ├── playlist.model.ts │ │ │ ├── schedule.model.ts │ │ │ ├── session.model.ts │ │ │ ├── speaker.model.ts │ │ │ ├── stage.model.ts │ │ │ ├── state.model.ts │ │ │ ├── support.model.ts │ │ │ └── user.model.ts │ │ ├── nodemon.json │ │ ├── routes │ │ │ └── routes.ts │ │ ├── server.ts │ │ ├── services │ │ │ ├── auth.service.ts │ │ │ ├── chat.service.ts │ │ │ ├── clipEditor.service.ts │ │ │ ├── event.service.ts │ │ │ ├── marker.service.ts │ │ │ ├── nft.service.ts │ │ │ ├── organization.service.ts │ │ │ ├── playlist.service.ts │ │ │ ├── schedule-import.service.ts │ │ │ ├── session.service.ts │ │ │ ├── speaker.service.ts │ │ │ ├── stage.service.ts │ │ │ ├── state.service.ts │ │ │ ├── stripe.service.ts │ │ │ ├── support.service.ts │ │ │ └── user.service.ts │ │ ├── swagger │ │ │ └── swagger.json │ │ └── utils │ │ │ ├── ai.chat.ts │ │ │ ├── ai.transcribes.ts │ │ │ ├── api.response.ts │ │ │ ├── google-sheet.ts │ │ │ ├── livepeer.ts │ │ │ ├── logger.ts │ │ │ ├── mail.service.ts │ │ │ ├── nft.storage.ts │ │ │ ├── oauth.ts │ │ │ ├── pinecone.ts │ │ │ ├── pulse.cron.ts │ │ │ ├── redis.ts │ │ │ ├── s3.ts │ │ │ ├── util.ts │ │ │ ├── validateEnv.ts │ │ │ ├── validateWebhook.ts │ │ │ └── youtube.ts │ ├── templates │ │ ├── invite.html │ │ └── login.html │ ├── test │ │ ├── auth.ts │ │ ├── event.spec.ts │ │ ├── global.ts │ │ ├── organization.spec.ts │ │ ├── session.spec.ts │ │ ├── speaker.spec.ts │ │ ├── stage.spec.ts │ │ └── utils │ │ │ └── generateRandomString.ts │ ├── tsconfig.json │ ├── tsconfig.prod.json │ ├── tsoa.json │ └── workers │ │ ├── clips │ │ ├── Dockerfile │ │ ├── Dockerfile.dev │ │ ├── index.ts │ │ └── nodemon.json │ │ ├── session-transcriptions │ │ ├── Dockerfile │ │ ├── Dockerfile.dev │ │ ├── index.ts │ │ └── nodemon.json │ │ ├── session-translations │ │ ├── Dockerfile │ │ ├── Dockerfile.dev │ │ ├── index.ts │ │ └── nodemon.json │ │ ├── stage-transcriptions │ │ ├── Dockerfile │ │ ├── Dockerfile.dev │ │ ├── index.ts │ │ └── nodemon.json │ │ └── video-importer │ │ ├── Dockerfile │ │ ├── Dockerfile.dev │ │ ├── index.ts │ │ └── nodemon.json └── video-uploader │ ├── .gitignore │ ├── Dockerfile.dev │ ├── docker-entrypoint.sh │ ├── package.json │ ├── src │ ├── config │ │ └── index.ts │ ├── stage-transcriptions-consumer.ts │ ├── utils │ │ ├── errors.ts │ │ ├── ffmpeg.ts │ │ ├── helper.ts │ │ ├── logger.ts │ │ ├── mq.ts │ │ ├── redis.ts │ │ ├── twitter.ts │ │ └── youtube.ts │ └── video-consumer.ts │ ├── tsconfig.json │ └── tsconfig.prod.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - Browser [e.g. chrome, safari] *Required* 28 | - OS: [e.g. iOS] *Optional* 29 | - Version [e.g. 22] *Optional* 30 | 31 | **Smartphone (please complete the following information):** 32 | - Browser [e.g. stock browser, safari] *Required* 33 | - Device: [e.g. iPhone6] *Optional* 34 | - OS: [e.g. iOS8.1] *Optional* 35 | - Version [e.g. 22] *Optional* 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: As a [User, Event, Developer, etc.], 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | 19 | **Screenshots** 20 | Do you have an example from another platform, or prototype you made by yourself? 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Define a new task related to a User Story 4 | title: [TASK] 5 | labels: task 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | [Provide a clear and concise description of the task] 12 | 13 | ## Related User Story 14 | Include the related User Story #< User Story > 15 | 16 | ## Objectives 17 | - [ ] [Objective 1] 18 | - [ ] [Objective 2] 19 | - [ ] [Objective 3] 20 | 21 | ## Acceptance Criteria 22 | - [ ] [Criterion 1] 23 | - [ ] [Criterion 2] 24 | - [ ] [Criterion 3] 25 | 26 | ## Additional Information 27 | [Any extra details, context, or resources that might be helpful] 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/usabilty_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Usability issue 3 | about: Report a usability problem to help us improve the user experience 4 | title: "[USABILITY]" 5 | labels: usability 6 | assignees: '' 7 | --- 8 | 9 | **Describe the usability issue** 10 | A clear and concise description of what the usability problem is. 11 | 12 | **To Reproduce** 13 | Steps to encounter the usability issue: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. Observe difficulty in '....' 18 | 19 | **Expected user experience** 20 | A clear and concise description of what you expected the user experience to be. 21 | 22 | **Actual user experience** 23 | A clear and concise description of the actual, problematic user experience. 24 | 25 | **Screenshots or screen recordings** 26 | If applicable, add screenshots or screen recordings to help explain the usability issue. 27 | 28 | **Accessibility considerations** 29 | Does this usability issue impact accessibility? If so, please provide details. 30 | 31 | **Additional context** 32 | Add any other context about the usability issue here. 33 | -------------------------------------------------------------------------------- /.github/workflows/generate.yml: -------------------------------------------------------------------------------- 1 | name: Generate assets 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 15 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '20' 19 | cache: 'yarn' 20 | - run: yarn install --frozen-lockfile 21 | - run: yarn run:generate 22 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | name: Prettier 2 | 3 | # on: 4 | # pull_request: 5 | # branches: [main, develop] 6 | # push: 7 | # branches: [main, develop] 8 | 9 | # Prevent multiple runs of the same workflow on the same ref 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | format: 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | steps: 19 | - name: Check out Git repository 20 | uses: actions/checkout@v4 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '20' 25 | cache: 'yarn' 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | - name: Format code with Prettier 29 | run: yarn prettier:fix 30 | - name: Commit changes 31 | uses: stefanzweifel/git-auto-commit-action@v5 32 | with: 33 | commit_message: "style: format code with prettier" 34 | branch: ${{ github.head_ref }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | undefined/* 4 | undefined 5 | # dependencies 6 | node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | .yarn/* 11 | .yarn 12 | 13 | # testing 14 | /coverage 15 | /playwright 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # local env files 34 | .env 35 | .env*.local 36 | test.env 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | error.log 45 | test.ts 46 | 47 | # Google Cloud secrets 48 | google_client_secret.json 49 | google_sa_secret.json 50 | 51 | # Generated assets 52 | /assets 53 | 54 | # Sentry Config File 55 | .sentryclirc 56 | 57 | .yarn/* 58 | .yarn 59 | .yarn/ 60 | 61 | 62 | *.log 63 | *.mp4 64 | 65 | # To avoid pushing the local db connection string 66 | packages/server/src/databases/index.ts -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const buildNextEslintCommand = (filenames) => 4 | `yarn next:lint --fix --file ${filenames 5 | .map((f) => path.relative(path.join("packages", "app"), f)) 6 | .join(" --file ")}`; 7 | 8 | // const checkTypesNextCommand = () => "yarn next:check-types"; 9 | const checkPrettierCommand = () => "yarn prettier:check"; 10 | 11 | module.exports = { 12 | "packages/app/**/*.{ts,tsx}": [ 13 | buildNextEslintCommand, 14 | checkPrettierCommand, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | /packages/tools/av-tools -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /docker-compose/build-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -f packages/server/workers/session-transcriptions/Dockerfile . -t session-transcriptions:1 4 | docker build -f packages/server/workers/stage-transcriptions/Dockerfile . -t stage-transcriptions:1 5 | docker build -f packages/server/src/Dockerfile . -t server:1 -------------------------------------------------------------------------------- /docker-compose/create-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to extract value from _FILE suffix variables 4 | extract_secret_value() { 5 | local var_name=$1 6 | local var_value=$2 7 | 8 | # Remove _FILE suffix for secret name 9 | secret_name=$(echo "$var_name" | sed 's/_FILE$//') 10 | secret_name=$(echo "$secret_name" | tr '[:upper:]' '[:lower:]') 11 | 12 | # Create Docker secret 13 | echo "Creating secret: $secret_name" 14 | echo "$var_value" | docker secret create "$secret_name" - 15 | } 16 | 17 | # Read .env file line by line 18 | while IFS='=' read -r key value; do 19 | # Skip empty lines and comments 20 | if [[ -z "$key" ]] || [[ "$key" =~ ^# ]]; then 21 | continue 22 | fi 23 | 24 | # Clean up key and value 25 | key=$(echo "$key" | xargs) 26 | value=$(echo "$value" | xargs) 27 | 28 | # Process only *_FILE variables that have values 29 | if [[ "$key" == *"_FILE"* ]] && [[ ! -z "$value" ]]; then 30 | # Remove any surrounding quotes from value 31 | value=$(echo "$value" | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//") 32 | extract_secret_value "$key" "$value" 33 | fi 34 | done < .env 35 | 36 | echo "Docker secrets creation completed!" -------------------------------------------------------------------------------- /docker-compose/readme.md: -------------------------------------------------------------------------------- 1 | ## To run dev 2 | ``` 3 | docker compose --env-file .env -f docker-compose-dev.yml up --build 4 | ``` 5 | 6 | ## Docker stack deploy 7 | docker stack deploy -c docker-compose/docker-compose.yml streameth-build --detach=false --build -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x848488156d9bfc5356583da57253193a258acebcdbc3ba2a10dc8ef81bd815fe" 4 | } 5 | } -------------------------------------------------------------------------------- /packages/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react-hooks/exhaustive-deps": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | /playwright 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | *.env 31 | .env.* 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | error.log 40 | test.ts 41 | 42 | # Google Cloud secrets 43 | google_client_secret.json 44 | google_sa_secret.json 45 | 46 | # Generated assets 47 | /assets 48 | 49 | # Sentry Config File 50 | .sentryclirc 51 | /test-results/ 52 | /playwright-report/ 53 | /blob-report/ 54 | /playwright/.cache/ 55 | -------------------------------------------------------------------------------- /packages/app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": true, 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "always", 11 | "endOfLine": "lf", 12 | "quoteProps": "as-needed", 13 | "jsxSingleQuote": false, 14 | "proseWrap": "preserve", 15 | "htmlWhitespaceSensitivity": "css" 16 | } 17 | -------------------------------------------------------------------------------- /packages/app/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # Use Node.js LTS (Long Term Support) as base image 2 | FROM node:18-alpine 3 | 4 | # Add Python and build dependencies 5 | RUN apk add --no-cache python3 python3-dev py3-pip make g++ gcc 6 | 7 | # Set working directory 8 | WORKDIR /app 9 | 10 | # Copy package files first 11 | COPY package.json yarn.lock ./ 12 | COPY packages/*/package.json ./packages/*/ 13 | 14 | # Install dependencies 15 | RUN yarn install --frozen-lockfile 16 | 17 | # Copy the rest of the source code 18 | COPY . . 19 | 20 | # Expose the default Next.js port 21 | EXPOSE 3000 22 | 23 | # Set environment variables 24 | ENV NODE_ENV=development 25 | ENV NEXT_TELEMETRY_DISABLED=1 26 | 27 | # Start the development server 28 | CMD ["yarn", "dev:app"] 29 | -------------------------------------------------------------------------------- /packages/app/app/(legal)/data-request/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { Card, CardTitle, CardFooter } from '@/components/ui/card'; 4 | import Image from 'next/image'; 5 | import Footer from '@/components/Layout/Footer'; 6 | import CreateRequest from './components/createRequest'; 7 | 8 | const DataRequest = async () => { 9 | 10 | return ( 11 |
12 |
13 | 14 | 15 | {'StreamETH 21 | 22 | 23 | 24 |
25 |
27 | ); 28 | }; 29 | 30 | export default DataRequest; 31 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/[event]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import { fetchEvent } from "@/lib/services/eventService"; 3 | 4 | const Layout = async ({ 5 | children, 6 | params: paramsPromise, 7 | }: { 8 | children: React.ReactNode; 9 | params: Promise<{ 10 | organization: string; 11 | event: string; 12 | }>; 13 | }) => { 14 | const params = await paramsPromise; 15 | const event = await fetchEvent({ 16 | eventSlug: params.event, 17 | }); 18 | 19 | if (!event) { 20 | return notFound(); 21 | } 22 | const style = { 23 | "--colors-accent": event.accentColor, 24 | backgroundColor: event.accentColor, 25 | } as React.CSSProperties; 26 | 27 | return ( 28 |
29 |
30 | {children} 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Layout; 37 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/[event]/speakers/components/SpeakerModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CredenzaContent, 3 | CredenzaDescription, 4 | CredenzaTitle, 5 | CredenzaHeader, 6 | CredenzaBody, 7 | } from '@/components/ui/crezenda'; 8 | 9 | import Link from 'next/link'; 10 | import { IExtendedSpeaker, IExtendedSession } from '@/lib/types'; 11 | 12 | interface Params { 13 | speaker: IExtendedSpeaker; 14 | sessions?: IExtendedSession[]; 15 | } 16 | 17 | const SpeakerModal = ({ speaker }: Params) => { 18 | return ( 19 | 20 | 21 | {speaker?.name} 22 | 23 | {speaker?.twitter && ( 24 | 25 | Follow on X 26 | 27 | )} 28 | 29 | 30 | {speaker?.bio} 31 | 32 | ); 33 | }; 34 | 35 | export default SpeakerModal; 36 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/[event]/speakers/page.tsx: -------------------------------------------------------------------------------- 1 | import SpeakerPageComponent from "./components/SpeakerComponent"; 2 | import EmbedLayout from "@/components/Layout/EmbedLayout"; 3 | import { fetchEvent } from "@/lib/services/eventService"; 4 | import { notFound } from "next/navigation"; 5 | import { EventPageProps } from "@/lib/types"; 6 | 7 | const SpeakerPage = async ({ params: paramsPromise }: EventPageProps) => { 8 | const params = await paramsPromise; 9 | 10 | const event = await fetchEvent({ 11 | eventId: params.event, 12 | }); 13 | 14 | if (!event) return notFound(); 15 | 16 | return ( 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default SpeakerPage; 24 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/[event]/stage/components/StagePreview.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Card, CardHeader } from '@/components/ui/card'; 3 | import Thumbnail from '@/components/misc/VideoCard/thumbnail'; 4 | import useSearchParams from '@/lib/hooks/useSearchParams'; 5 | import { IExtendedStage } from '@/lib/types'; 6 | 7 | export default function StagePreview({ 8 | stage, 9 | eventCover, 10 | }: { 11 | stage: IExtendedStage; 12 | organization: string; 13 | event: string; 14 | eventCover: string | undefined; 15 | }) { 16 | const { handleTermChange } = useSearchParams(); 17 | 18 | if (!stage?.streamSettings?.streamId) return null; 19 | 20 | return ( 21 | 24 | handleTermChange([ 25 | { 26 | key: 'livestream', 27 | value: stage._id as string, 28 | }, 29 | ]) 30 | } 31 | > 32 | 33 | 34 |

{stage.name}

35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/components/ShareVideoMenuItem.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import ShareButton from '@/components/misc/interact/ShareButton'; 3 | import { buttonVariants } from '@/components/ui/button'; 4 | import React, { useEffect, useState } from 'react'; 5 | 6 | const ShareVideoMenuItem = ({ url }: { url: string }) => { 7 | const [currentUrl, setCurrentUrl] = useState(''); 8 | useEffect(() => { 9 | // This code will only run on the client side 10 | if (typeof window === 'undefined') return; 11 | setCurrentUrl(window.location.origin); 12 | }, []); 13 | return ( 14 |
e.stopPropagation()}> 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default ShareVideoMenuItem; 21 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/components/VideoDownload.tsx: -------------------------------------------------------------------------------- 1 | import { Livepeer } from 'livepeer'; 2 | import { ArrowDownToLine, Download } from 'lucide-react'; 3 | import { Button } from '@/components/ui/button'; 4 | 5 | const VideoDownload = async ({ assetId }: { assetId: string }) => { 6 | const livepeer = new Livepeer({ 7 | apiKey: process.env.LIVEPEER_API_KEY, 8 | }); 9 | 10 | if (!assetId) return null; 11 | 12 | const asset = (await livepeer.asset.get(assetId)).asset; 13 | 14 | if (!asset) return null; 15 | 16 | return ( 17 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default VideoDownload; 32 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/components/ViewCounts.tsx: -------------------------------------------------------------------------------- 1 | import { fetchSessionMetrics } from '@/lib/services/sessionService'; 2 | 3 | const ViewCounts = async ({ 4 | playbackId, 5 | className, 6 | }: { 7 | playbackId: string; 8 | className?: string; 9 | }) => { 10 | const data = await fetchSessionMetrics({ playbackId }); 11 | 12 | return ( 13 |

14 | {data.viewCount} views 15 |

16 | ); 17 | }; 18 | 19 | export default ViewCounts; 20 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/livestream/components/ArchiveVideosSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardDescription, CardHeader } from '@/components/ui/card'; 2 | 3 | const ArchiveVideoSkeleton = () => { 4 | return ( 5 |
6 | {[...Array(3)].map((_, index) => ( 7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 | 16 |
17 | 18 | ))} 19 |
20 | ); 21 | }; 22 | 23 | export default ArchiveVideoSkeleton; 24 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/livestream/components/PaginationSkeleteon.tsx: -------------------------------------------------------------------------------- 1 | const PaginationSkeleton = () => { 2 | return ( 3 |
4 |
5 | {/* Left Arrow Skeleton */} 6 |
7 |
8 |
9 | 10 | {/* Page Number Skeleton */} 11 |
12 |
13 |
14 | 15 | {/* Right Arrow Skeleton */} 16 |
17 |
18 |
19 |
20 |
21 | ); 22 | }; 23 | 24 | export default PaginationSkeleton; 25 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/loading.tsx: -------------------------------------------------------------------------------- 1 | 2 | const Loading = () => { 3 | return
Loading...
; 4 | }; 5 | 6 | export default Loading; 7 | -------------------------------------------------------------------------------- /packages/app/app/[organization]/videos/components/eventSelect.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import useSearchParams from '@/lib/hooks/useSearchParams'; 3 | import Combobox from '@/components/ui/combo-box'; 4 | import { IExtendedEvent } from '@/lib/types'; 5 | 6 | const EventSelect = ({ events }: { events: IExtendedEvent[] }) => { 7 | const { handleTermChange, searchParams } = useSearchParams(); 8 | const event = searchParams.get('event') || ''; 9 | 10 | if (events.length === 0) { 11 | return null; 12 | } 13 | 14 | return ( 15 | { 19 | handleTermChange([ 20 | { 21 | key: 'event', 22 | value: value, 23 | }, 24 | ]); 25 | }} 26 | items={[ 27 | { label: 'None', value: '' }, 28 | ...events.map((event) => ({ 29 | label: event.name, 30 | value: event._id, 31 | })), 32 | ]} 33 | /> 34 | ); 35 | }; 36 | 37 | export default EventSelect; 38 | -------------------------------------------------------------------------------- /packages/app/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from '@/auth'; 2 | 3 | export const { GET, POST } = handlers; 4 | -------------------------------------------------------------------------------- /packages/app/app/api/google/request/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | export async function GET(request: NextRequest) { 4 | const searchParams = request.nextUrl.searchParams; 5 | const state = searchParams.get('state'); 6 | // const GOOGLE_OAUTH_SECRET = process.env.GOOGLE_OAUTH_SECRET || ''; 7 | // const oAuthSecret: any = JSON.parse(GOOGLE_OAUTH_SECRET); 8 | // const clientId = oAuthSecret.web.client_id; 9 | // const redirectUri = oAuthSecret.web.redirect_uri; 10 | 11 | const googleAuthUrl = process.env.GOOGLE_OAUTH_URL; 12 | 13 | const authUrl = `${googleAuthUrl}&state=${state}`; 14 | 15 | try { 16 | return NextResponse.redirect(authUrl); 17 | } catch (err) { 18 | return NextResponse.json( 19 | { error: err }, 20 | { 21 | status: 500, 22 | } 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/app/api/video-download/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | export const GET = async (req: NextRequest): Promise => { 4 | try { 5 | const url = new URL(req.url); 6 | 7 | const playbackId = url.searchParams.get('playbackId'); 8 | const Authorization = process.env.LIVEPEER_API_KEY; 9 | if (!playbackId) { 10 | return NextResponse.json('Missing playbackId', { status: 401 }); 11 | } 12 | if (!Authorization) { 13 | return NextResponse.json('Missing Authorization Token', { 14 | status: 401, 15 | }); 16 | } 17 | 18 | const response = await fetch( 19 | `https://livepeer.studio/api/playback/${playbackId}`, 20 | { 21 | method: 'GET', 22 | headers: { 23 | Authorization: Authorization, 24 | }, 25 | } 26 | ); 27 | 28 | const data = await response.json(); 29 | 30 | return NextResponse.json(data); 31 | } catch (error) { 32 | return NextResponse.json('Internal Server Error', { status: 500 }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /packages/app/app/auth/logout/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | import { signOut } from 'next-auth/react'; 5 | import { useRouter } from 'next/navigation'; 6 | 7 | const LogoutPage = () => { 8 | const router = useRouter(); 9 | 10 | useEffect(() => { 11 | const performLogout = async () => { 12 | await signOut({ redirect: false }); 13 | router.push('/auth/login'); 14 | }; 15 | 16 | performLogout(); 17 | }, [router]); 18 | 19 | return ( 20 |
21 |

Logging out...

22 |

23 | Please wait while we securely log you out. 24 |

25 |
26 | ); 27 | }; 28 | 29 | export default LogoutPage; 30 | -------------------------------------------------------------------------------- /packages/app/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function Component() { 4 | return ( 5 |
6 |
7 |
8 | 404 9 |
10 |

11 | Oops, the page you are looking for could not be found. 12 |

13 |
14 | 19 | Go Back 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/app/speaker/[session]/components/ClientSidePlayer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PlayerWithControls } from '@/components/ui/Player'; 4 | import { Src } from '@livepeer/react'; 5 | 6 | export default function ClientSidePlayer({ 7 | name, 8 | thumbnail, 9 | src, 10 | caption, 11 | }: { 12 | name: string; 13 | thumbnail: string; 14 | src: Src[]; 15 | caption?: string; 16 | }) { 17 | return ( 18 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/app/speaker/[session]/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import HomePageNavbar from '@/components/Layout/HomePageNavbar'; 4 | import Footer from '@/components/Layout/Footer'; 5 | 6 | const Layout = async ({ children }: { children: React.ReactNode }) => { 7 | return ( 8 |
9 | 14 |
{children}
15 |
16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Layout; 23 | -------------------------------------------------------------------------------- /packages/app/app/studio/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Support from '@/components/misc/Support'; 2 | 3 | const StudioLayout = async (props: { children: React.ReactNode }) => { 4 | return ( 5 |
6 |
7 | {props.children} 8 | 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default StudioLayout; 15 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(no-side-bar)/clips/page.tsx: -------------------------------------------------------------------------------- 1 | const ClipsPage = () => { 2 | return
ClipsPage
; 3 | }; 4 | 5 | export default ClipsPage; 6 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(no-side-bar)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavbarStudio from '@/components/Layout/NavbarStudio'; 3 | 4 | const Layout = async ({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) => { 9 | return ( 10 |
11 | 12 |
13 |
14 |
{children}
15 |
16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Layout; 22 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/destinations/components/TwitterConnectButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import Link from 'next/link'; 5 | import React from 'react'; 6 | import { SiX } from 'react-icons/si'; 7 | 8 | const TwitterConnectButton = ({ state }: { state?: string }) => { 9 | return ( 10 | 11 | 14 | 15 | ); 16 | }; 17 | 18 | export default TwitterConnectButton; 19 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/destinations/components/YoutubeConnectButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '@/components/ui/button'; 3 | import Link from 'next/link'; 4 | import React from 'react'; 5 | import Image from 'next/image'; 6 | 7 | const YoutubeConnectButton = ({ state }: { state?: string }) => { 8 | return ( 9 | 10 | 20 | 21 | ); 22 | }; 23 | 24 | export default YoutubeConnectButton; 25 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NavbarStudio from "@/components/Layout/NavbarStudio"; 3 | import SidebarMenu from "@/components/Sidebar/SidebarMenu"; 4 | 5 | const Layout = async ({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) => { 10 | return ( 11 |
12 | 13 |
14 | 15 |
16 |
{children}
17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Layout; 24 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/library/[session]/components/form/utils.ts: -------------------------------------------------------------------------------- 1 | import { IExtendedSession } from '@/lib/types'; 2 | import * as z from 'zod'; 3 | import { sessionSchema } from '@/lib/schema'; 4 | 5 | export const createSessionUpdatePayload = ( 6 | values: z.infer, 7 | session: IExtendedSession 8 | ) => ({ 9 | session: { 10 | ...values, 11 | _id: session._id, 12 | organizationId: session.organizationId, 13 | eventId: session.eventId, 14 | stageId: session.stageId, 15 | start: session.start ?? Number(new Date()), 16 | end: session.end ?? Number(new Date()), 17 | speakers: session.speakers ?? [], 18 | type: session.type, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/library/components/EmptyLibrary.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import EmptyFolder from '@/lib/svg/EmptyFolder'; 4 | import UploadVideoDialog from './UploadVideoDialog'; 5 | 6 | const EmptyLibrary = () => { 7 | return ( 8 |
9 | 10 |
11 |

The library is empty

12 |

13 | Upload your first video to get started! 14 |

15 |
16 | 17 |
18 | ); 19 | }; 20 | 21 | export default EmptyLibrary; 22 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/library/components/tabs/PlaylistsTab.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { CreatePlaylistDialog } from '../CreatePlaylistDialog'; 4 | import PlaylistTable, { PlaylistTableSkeleton } from '../PlaylistTable'; 5 | import { fetchOrganizationPlaylists } from '@/lib/services/playlistService'; 6 | 7 | interface PlaylistsTabProps { 8 | organizationId: string; 9 | } 10 | 11 | const PlaylistsTab = async ({ organizationId }: PlaylistsTabProps) => { 12 | const playlists = await fetchOrganizationPlaylists({ organizationId }); 13 | 14 | return ( 15 | <> 16 |
17 | 18 |
19 | 20 | 21 | ); 22 | }; 23 | 24 | export default PlaylistsTab; -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/library/playlists/[playlistId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { fetchPlaylist } from "@/lib/services/playlistService"; 4 | import { fetchSession } from "@/lib/services/sessionService"; 5 | import { notFound } from "next/navigation"; 6 | import PlaylistDetail from "./components/PlaylistDetail"; 7 | 8 | const PlaylistDetailPage = async ({ 9 | params: paramsPromise, 10 | }: { 11 | params: Promise<{ organization: string; playlistId: string }>; 12 | }) => { 13 | const params = await paramsPromise; 14 | 15 | const playlist = await fetchPlaylist({ 16 | organizationId: params.organization, 17 | playlistId: params.playlistId, 18 | }).catch(() => null); 19 | 20 | if (!playlist) { 21 | notFound(); 22 | } 23 | 24 | const sessionsPromises = playlist.sessions.map((id) => 25 | fetchSession({ session: id.toString() }), 26 | ); 27 | const sessions = (await Promise.all(sessionsPromises)).filter( 28 | (session): session is NonNullable => session !== null, 29 | ); 30 | 31 | return ( 32 |
33 | 34 |
35 | ); 36 | }; 37 | 38 | export default PlaylistDetailPage; 39 | 40 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/livestreams/[streamId]/components/ClientSidePlayer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PlayerWithControls } from '@/components/ui/Player'; 4 | import { Src } from '@livepeer/react'; 5 | 6 | export default function ClientSidePlayer({ 7 | name, 8 | thumbnail, 9 | src, 10 | caption, 11 | }: { 12 | name: string; 13 | thumbnail: string; 14 | src: Src[]; 15 | caption?: string; 16 | }) { 17 | return ( 18 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/livestreams/[streamId]/components/StreamHeader.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { IExtendedStage } from '@/lib/types'; 4 | import React from 'react'; 5 | import Link from 'next/link'; 6 | import { ArrowLeft } from 'lucide-react'; 7 | import { Button } from '@/components/ui/button'; 8 | import { useOrganizationContext } from '@/lib/context/OrganizationContext'; 9 | 10 | const StreamHeader = ({ 11 | stream, 12 | isLiveStreamPage, 13 | }: { 14 | stream: IExtendedStage; 15 | isLiveStreamPage?: boolean; 16 | }) => { 17 | const { organizationId } = useOrganizationContext(); 18 | return ( 19 |
20 | {stream.name} 21 | 22 | {isLiveStreamPage && ( 23 | 24 | 28 | 29 | )} 30 |
31 | ); 32 | }; 33 | 34 | export default StreamHeader; 35 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/livestreams/components/ShareLivestream.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import ShareButton from '@/components/misc/interact/ShareButton'; 3 | import { useOrganizationContext } from '@/lib/context/OrganizationContext'; 4 | import React, { useEffect, useState } from 'react'; 5 | 6 | const ShareLivestream = ({ 7 | streamId, 8 | variant = 'ghost', 9 | }: { 10 | variant?: 'outline' | 'ghost' | 'primary' | 'default'; 11 | streamId: string; 12 | }) => { 13 | const { organizationId } = useOrganizationContext(); 14 | const [url, setUrl] = useState(''); 15 | useEffect(() => { 16 | // This code will only run on the client side 17 | if (typeof window === 'undefined') return; 18 | 19 | setUrl(window.location.origin); 20 | }, []); 21 | return ( 22 |
e.stopPropagation()}> 23 | 29 |
30 | ); 31 | }; 32 | 33 | export default ShareLivestream; 34 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/livestreams/page.tsx: -------------------------------------------------------------------------------- 1 | const LivestreamsPage = async () => { 2 | return 'livestream'; 3 | }; 4 | 5 | export default LivestreamsPage; 6 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/payments/components/ExpiredSubscriptionCard.tsx: -------------------------------------------------------------------------------- 1 | import { AlertCircle } from 'lucide-react'; 2 | import { Card } from '@/components/ui/card'; 3 | 4 | export function ExpiredSubscriptionCard() { 5 | return ( 6 | 7 |
8 | 9 |
10 |

Your subscription has expired

11 |

12 | Purchase a new subscription to continue streaming. 13 |

14 |
15 |
16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/payments/components/LoadingState.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from 'lucide-react'; 2 | 3 | export function LoadingState() { 4 | return ( 5 |
6 |
7 | 8 | Loading subscription details... 9 |
10 |
11 | ); 12 | } -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/settings/components/NavigationItem.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import useSearchParams from '@/lib/hooks/useSearchParams'; 3 | 4 | const NavigationItem = ({ lable, path }: { lable: string; path: string }) => { 5 | const { searchParams, handleTermChange } = useSearchParams(); 6 | 7 | const active = 8 | searchParams.get('settingsActiveTab') === path || 9 | (!searchParams.get('settingsActiveTab') && path === 'basicInfo'); 10 | return ( 11 |
16 | handleTermChange([ 17 | { 18 | key: 'settingsActiveTab', 19 | value: path, 20 | }, 21 | ]) 22 | } 23 | > 24 | 27 |
28 | ); 29 | }; 30 | 31 | export default NavigationItem; 32 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/(root)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import CreateOrganizationForm from "@/app/studio/(home)/components/CreateOrganizationForm"; 5 | import { 6 | Card, 7 | CardContent, 8 | CardDescription, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | import { useOrganizationContext } from "@/lib/context/OrganizationContext"; 13 | 14 | const Settings = () => { 15 | const { organization } = useOrganizationContext(); 16 | return ( 17 |
18 | 19 | 20 | Edit yout channel 21 | 22 | Header logo and description will appear on your channel page 23 | 24 | 25 | 26 | 30 | 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default Settings; 37 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/nfts/components/MintNftSort.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Combobox from '@/components/ui/combo-box'; 4 | import useSearchParams from '@/lib/hooks/useSearchParams'; 5 | import { eSort } from '@/lib/types'; 6 | 7 | import React from 'react'; 8 | 9 | const sortOptions = [ 10 | { value: 'desc_alpha', label: 'name' }, 11 | { value: 'desc_date', label: 'recent' }, 12 | ]; 13 | 14 | const MintNftSort = () => { 15 | const { searchParams, handleTermChange } = useSearchParams(); 16 | const currentSort = searchParams.get('sort') as eSort; 17 | 18 | return ( 19 |
20 |

Sort By

21 | 22 | { 27 | handleTermChange([{ key: 'sort', value: value }]); 28 | }} 29 | /> 30 |
31 | ); 32 | }; 33 | 34 | export default MintNftSort; 35 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/nfts/create/components/ShareCollection.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import ShareButton from '@/components/misc/interact/ShareButton'; 3 | import React, { useEffect, useState } from 'react'; 4 | 5 | const ShareCollection = ({ collectionId }: { collectionId?: string }) => { 6 | const [url, setUrl] = useState(''); 7 | useEffect(() => { 8 | // This code will only run on the client side 9 | if (typeof window === 'undefined') return; 10 | 11 | setUrl(window.location.origin); 12 | }, []); 13 | return ( 14 |
e.stopPropagation()}> 15 | 19 |
20 | ); 21 | }; 22 | 23 | export default ShareCollection; 24 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/nfts/create/components/ShareVideoNFT.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import ShareButton from '@/components/misc/interact/ShareButton'; 3 | import React, { useEffect, useState } from 'react'; 4 | 5 | const ShareVideoNFT = ({ 6 | collectionId, 7 | organization, 8 | }: { 9 | organization: string; 10 | collectionId: string; 11 | }) => { 12 | const [url, setUrl] = useState(''); 13 | useEffect(() => { 14 | // This code will only run on the client side 15 | if (typeof window === 'undefined') return; 16 | 17 | setUrl(window.location.origin); 18 | }, []); 19 | return ( 20 |
e.stopPropagation()}> 21 | 25 |
26 | ); 27 | }; 28 | 29 | export default ShareVideoNFT; 30 | -------------------------------------------------------------------------------- /packages/app/app/studio/[organization]/nfts/create/components/TransactionHash.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLinkIcon } from 'lucide-react'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | 5 | const TransactionHash = ({ hash }: { hash: string }) => { 6 | return ( 7 | 13 | View on Base Scan 14 | 15 | ); 16 | }; 17 | 18 | export default TransactionHash; 19 | -------------------------------------------------------------------------------- /packages/app/app/studio/layout.tsx: -------------------------------------------------------------------------------- 1 | import Support from '@/components/misc/Support'; 2 | import { fetchUserAction } from '@/lib/actions/users'; 3 | import { redirect } from 'next/navigation'; 4 | 5 | const StudioLayout = async (props: { children: React.ReactNode }) => { 6 | console.log('🚀 [StudioLayout] Fetching user data...'); 7 | const startTime = Date.now(); 8 | 9 | const user = await fetchUserAction(); 10 | 11 | console.log(`⏱️ [StudioLayout] User data fetch complete in ${Date.now() - startTime}ms:`, user ? '✅ User found' : '❌ User not found'); 12 | 13 | if (!user) { 14 | console.log('🔄 [StudioLayout] Redirecting to login page'); 15 | return redirect('/auth/login'); 16 | } 17 | 18 | return ( 19 |
20 |
21 | {props.children} 22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default StudioLayout; 29 | -------------------------------------------------------------------------------- /packages/app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/app/components/Layout/EmbedLayout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useContext, useEffect } from 'react'; 3 | import { TopNavbarContext } from '../../lib/context/TopNavbarContext'; 4 | 5 | const EmbedLayout = ({ children }: { children: React.ReactNode }) => { 6 | const { setShowNav } = useContext(TopNavbarContext); 7 | 8 | useEffect(() => { 9 | setShowNav(false); 10 | }, [setShowNav]); 11 | 12 | return children; 13 | }; 14 | 15 | export default EmbedLayout; 16 | -------------------------------------------------------------------------------- /packages/app/components/Layout/SwitchOrganization.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Combobox from '@/components/ui/combo-box'; 3 | import React from 'react'; 4 | import { useRouter } from 'next/navigation'; 5 | import { IExtendedOrganization } from '@/lib/types'; 6 | const SwitchOrganization = ({ 7 | organization, 8 | organizations = [], 9 | }: { 10 | organization?: string; 11 | organizations?: IExtendedOrganization[]; 12 | }) => { 13 | const router = useRouter(); 14 | 15 | return ( 16 |
17 | ({ 20 | label: org.name, 21 | value: org.slug!, 22 | logo: org.logo, 23 | })), 24 | ]} 25 | logo 26 | variant="ghost" 27 | value={organization || ''} 28 | setValue={(org) => { 29 | router.push(`/studio/${org}`); 30 | }} 31 | /> 32 |
33 | ); 34 | }; 35 | 36 | export default SwitchOrganization; 37 | -------------------------------------------------------------------------------- /packages/app/components/misc/ConnectWalletButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ConnectKitButton } from 'connectkit'; 3 | import { Button } from '@/components/ui/button'; 4 | interface ConnectWalletButtonProps { 5 | className?: string; 6 | btnText?: string; 7 | } 8 | 9 | export const ConnectWalletButton = ({ 10 | btnText = 'Connect Wallet', 11 | className, 12 | }: ConnectWalletButtonProps) => { 13 | return ( 14 | 15 | {({ isConnected, show, truncatedAddress, ensName }) => { 16 | return ( 17 | 22 | ); 23 | }} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/app/components/misc/CopyString.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { toast } from 'sonner'; 4 | import { Copy } from 'lucide-react'; 5 | 6 | const CopyItem = ({ item, itemName }: { item: string; itemName: string }) => { 7 | const handleCopy = () => { 8 | navigator.clipboard.writeText(item); 9 | toast.success(`Copied ${itemName} to your clipboard`); 10 | }; 11 | 12 | return ( 13 |
17 | {item} 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default CopyItem; 24 | -------------------------------------------------------------------------------- /packages/app/components/misc/CopyText.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { copyToClipboard } from '@/lib/utils/utils'; 3 | import { Copy } from 'lucide-react'; 4 | 5 | const CopyText = ({ 6 | label, 7 | text = '', 8 | width = '450px', 9 | classNames = '', 10 | }: { 11 | label: string; 12 | text?: string; 13 | width?: string; 14 | classNames?: string; 15 | }) => ( 16 |
20 |
21 |
22 |

{label}

23 |
24 |

{text}

25 |
26 |
27 | copyToClipboard(text)} 29 | className="h-4 w-4 cursor-pointer text-primary" 30 | /> 31 |
32 |
33 | ); 34 | export default CopyText; 35 | -------------------------------------------------------------------------------- /packages/app/components/misc/MarkdownDisplay.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import MDEditor from '@uiw/react-md-editor'; 3 | 4 | const MarkdownDisplay = ({ content }: { content: string }) => { 5 | return ( 6 | 14 | ); 15 | }; 16 | 17 | export default MarkdownDisplay; 18 | -------------------------------------------------------------------------------- /packages/app/components/misc/SearchBar/SearchResults.tsx: -------------------------------------------------------------------------------- 1 | import { IExtendedSession } from '@/lib/types'; 2 | 3 | interface SearchResultsProps { 4 | isLoading: boolean; 5 | searchResults: IExtendedSession[]; 6 | onSelect: (result: IExtendedSession) => void; 7 | } 8 | 9 | export function SearchResults({ 10 | isLoading, 11 | searchResults, 12 | onSelect, 13 | }: SearchResultsProps) { 14 | if (isLoading) { 15 | return Loading...; 16 | } 17 | 18 | return ( 19 |
20 | {searchResults.length > 0 && ( 21 |
22 |
Videos
23 | {searchResults.map((result) => ( 24 |
onSelect(result)} 26 | className="cursor-pointer p-1 hover:bg-gray-100" 27 | key={result._id.toString()} 28 | > 29 | {result.name} 30 |
31 | ))} 32 |
33 | )} 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/app/components/misc/TableSort.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import useSearchParams from '@/lib/hooks/useSearchParams'; 3 | import { eSort } from '@/lib/types'; 4 | import { ChevronsUpDown } from 'lucide-react'; 5 | 6 | import React from 'react'; 7 | 8 | const TableSort = ({ title, sortBy }: { title: string; sortBy: string }) => { 9 | const { searchParams, handleTermChange } = useSearchParams(); 10 | const currentSort = searchParams.get('sort') as eSort; 11 | 12 | const handleSortClick = () => { 13 | let newSort; 14 | if (sortBy === 'date') { 15 | newSort = 16 | currentSort === eSort.asc_date ? eSort.desc_date : eSort.asc_date; 17 | } else if (sortBy === 'name') { 18 | newSort = 19 | currentSort === eSort.asc_alpha ? eSort.desc_alpha : eSort.asc_alpha; 20 | } 21 | 22 | handleTermChange([{ key: 'sort', value: newSort as string }]); 23 | }; 24 | return ( 25 |
29 |

{title}

30 | 31 |
32 | ); 33 | }; 34 | 35 | export default TableSort; 36 | -------------------------------------------------------------------------------- /packages/app/components/misc/VideoCard/VideoCardSkeleton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { EllipsisVertical } from 'lucide-react'; 4 | 5 | const VideoCardSkeleton = () => { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | ); 19 | }; 20 | 21 | export const VideoCardSkeletonMobile = () => { 22 | return ( 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default VideoCardSkeleton; 34 | -------------------------------------------------------------------------------- /packages/app/components/misc/VideoCard/thumbnail.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import DefaultThumbnail from "@/lib/svg/DefaultThumbnail"; 5 | import { AspectRatio } from "@/components/ui/aspect-ratio"; 6 | 7 | type ThumbnailProps = { 8 | imageUrl?: string; 9 | fallBack?: string; 10 | }; 11 | 12 | export default function Thumbnail({ imageUrl, fallBack }: ThumbnailProps) { 13 | const srcUrl = imageUrl || fallBack; 14 | 15 | if (!srcUrl) { 16 | return ( 17 | 21 | 22 | 23 | ); 24 | } 25 | 26 | return ( 27 |
28 | Session image 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /packages/app/components/misc/form/colorPicker.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { SketchPicker } from 'react-color'; 3 | import { Input } from '@/components/ui/input'; 4 | import { useState } from 'react'; 5 | import { 6 | Popover, 7 | PopoverContent, 8 | PopoverTrigger, 9 | } from '@/components/ui/popover'; 10 | 11 | const ColorPicker = ({ 12 | color, 13 | onChange, 14 | }: { 15 | color: string | undefined; 16 | onChange: (color: string) => void; 17 | }) => { 18 | const [isOpen, setIsOpen] = useState(true); 19 | return ( 20 | 21 | 22 | setIsOpen(true)} 30 | /> 31 | 32 | {isOpen && ( 33 | 34 | { 37 | onChange(color.hex); 38 | }} 39 | /> 40 | 41 | )} 42 | 43 | ); 44 | }; 45 | 46 | export default ColorPicker; 47 | -------------------------------------------------------------------------------- /packages/app/components/sessions/LiveIndicator.tsx: -------------------------------------------------------------------------------- 1 | export default function LiveIndicator() { 2 | return
Live
; 3 | } 4 | -------------------------------------------------------------------------------- /packages/app/components/sessions/TranscriptionModal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from '../ui/dialog'; 10 | import { Button } from '../ui/button'; 11 | import { IExtendedSession } from '@/lib/types'; 12 | import { formatTimestamp } from '@/lib/utils/utils'; 13 | 14 | const TranscriptionModal = ({ video }: { video: IExtendedSession }) => { 15 | if (!video?.transcripts?.text) return null; 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | Transcript for {video?.name} 23 |
{video?.transcripts?.text}
24 |
25 |
26 | ); 27 | }; 28 | 29 | export default TranscriptionModal; 30 | -------------------------------------------------------------------------------- /packages/app/components/ui/TitleTextItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TitleTextItem = ({ 4 | title, 5 | text, 6 | }: { 7 | title: string; 8 | text?: string | number; 9 | }) => { 10 | return ( 11 |
12 |

{title}

13 |

{text}

14 |
15 | ); 16 | }; 17 | 18 | export default TitleTextItem; 19 | -------------------------------------------------------------------------------- /packages/app/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root; 6 | 7 | export { AspectRatio }; 8 | -------------------------------------------------------------------------------- /packages/app/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/utils/utils'; 5 | 6 | const badgeVariants = cva( 7 | 'inline-flex items-center rounded-xl px-3 py-1.5 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | { 9 | variants: { 10 | variant: { 11 | default: ' bg-white border hover:bg-primary/80', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'default', 21 | }, 22 | } 23 | ); 24 | 25 | export interface BadgeProps 26 | extends React.HTMLAttributes, 27 | VariantProps {} 28 | 29 | function Badge({ className, variant, ...props }: BadgeProps) { 30 | return ( 31 |
32 | ); 33 | } 34 | 35 | export { Badge, badgeVariants }; 36 | -------------------------------------------------------------------------------- /packages/app/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 5 | import { Check } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils/utils'; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /packages/app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils/utils'; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = 'Input'; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /packages/app/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | 7 | import { cn } from '@/lib/utils/utils'; 8 | 9 | type LabelProps = { 10 | required?: boolean; 11 | }; 12 | 13 | const labelVariants = cva( 14 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 15 | ); 16 | 17 | const Label = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef & 20 | LabelProps & 21 | VariantProps 22 | >(({ className, required, ...props }, ref) => ( 23 |
24 | 29 | {required && *} 30 |
31 | )); 32 | Label.displayName = LabelPrimitive.Root.displayName; 33 | 34 | export { Label }; 35 | -------------------------------------------------------------------------------- /packages/app/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as ProgressPrimitive from '@radix-ui/react-progress'; 5 | 6 | import { cn } from '@/lib/utils/utils'; 7 | 8 | const Progress = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, value, ...props }, ref) => ( 12 | 20 | 24 | 25 | )); 26 | Progress.displayName = ProgressPrimitive.Root.displayName; 27 | 28 | export { Progress }; 29 | -------------------------------------------------------------------------------- /packages/app/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 5 | 6 | import { cn } from '@/lib/utils/utils'; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = 'horizontal', decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /packages/app/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils/utils'; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /packages/app/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Toaster as Sonner } from 'sonner'; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = 'system' } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster }; 32 | -------------------------------------------------------------------------------- /packages/app/components/ui/text-placeholder.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { toast } from 'sonner'; 3 | import { Copy } from 'lucide-react'; 4 | 5 | export default function TextPlaceholder({ text }: { text: string }) { 6 | const handleCopy = (item: string) => { 7 | navigator.clipboard.writeText(item); 8 | toast.success('Copied to your clipboard'); 9 | }; 10 | 11 | return ( 12 |
handleCopy(text)} 15 | > 16 | {text} 17 | 18 | Copy to clipboard 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/app/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils/utils'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |