diff --git a/.github/workflows/cd_PR.yml b/.github/workflows/cd_PR.yml new file mode 100644 index 000000000..ed1fa2dba --- /dev/null +++ b/.github/workflows/cd_PR.yml @@ -0,0 +1,71 @@ +name: Pull Request build+deploy + +on: + pull_request: + types: ['opened', 'edited', 'synchronize'] + branches: + - '**' + +jobs: + deploy: + environment: branch-deploy + name: Build and deploy + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Set up Docker + uses: docker/setup-docker-action@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Setup deployment + uses: bobheadxi/deployments@v1 + id: deployment + with: + step: start + token: ${{ secrets.GITHUB_TOKEN }} + env: branch-deploy-${{ github.head_ref }} + ref: ${{ github.head_ref }} + + - name: Install dependencies + run: | + pnpm install + pnpm build + + - name: Deploy to Netlify + uses: nwtgck/actions-netlify@v3.0 + with: + publish-dir: '.next' + production-branch: main + github-token: ${{ secrets.GITHUB_TOKEN }} + deploy-message: "Deploy from GitHub Actions" + enable-pull-request-comment: false + enable-commit-comment: true + overwrites-pull-request-comment: true + alias: deploy-preview-${{ github.event.number }}-100xdevs + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + timeout-minutes: 1 + + - name: Update deployment status + uses: bobheadxi/deployments@v1 + if: always() + with: + step: finish + token: ${{ secrets.GITHUB_TOKEN }} + status: ${{ job.status }} + deployment_id: ${{ steps.deployment.outputs.deployment_id }} + logs: ${{ steps.deploy-netlify.outputs.netlify_logs_url }} + env_url: ${{ steps.deploy-netlify.outputs.netlify_preview_url }} diff --git a/.github/workflows/pr_deploy_cleanup.yml b/.github/workflows/pr_deploy_cleanup.yml new file mode 100644 index 000000000..37bee2fea --- /dev/null +++ b/.github/workflows/pr_deploy_cleanup.yml @@ -0,0 +1,56 @@ +# name: Cleanup Old Netlify Deployments + +# on: +# schedule: +# # Run at 00:00 UTC every Sunday +# - cron: '0 0 * * 0' +# # Optional: Allow manual trigger +# workflow_dispatch: + +# jobs: +# cleanup: +# name: Cleanup Old Netlify Deployments +# runs-on: ubuntu-latest +# steps: +# - name: Install Netlify CLI +# run: npm install -g netlify-cli + +# - name: Authenticate with Netlify +# run: netlify login --auth ${{ secrets.NETLIFY_AUTH_TOKEN }} + +# - name: List and delete old deployments +# run: | +# # Get current date in seconds since epoch +# CURRENT_DATE=$(date +%s) + +# # Calculate the cutoff date (60 days ago) in seconds +# SIXTY_DAYS_IN_SECONDS=$((60 * 24 * 60 * 60)) +# CUTOFF_DATE=$((CURRENT_DATE - SIXTY_DAYS_IN_SECONDS)) + +# echo "Fetching deployments for site ${{ secrets.NETLIFY_SITE_ID }}..." + +# # List all deployments and filter those older than 60 days +# DEPLOYMENTS=$(netlify api listSiteDeploys --data "{\"site_id\":\"${{ secrets.NETLIFY_SITE_ID }}\"}") + +# # Process each deployment +# echo "$DEPLOYMENTS" | jq -c '.[]' | while read -r deploy; do +# DEPLOY_ID=$(echo "$deploy" | jq -r '.id') +# CREATED_AT=$(echo "$deploy" | jq -r '.created_at') + +# # Convert ISO date to seconds since epoch +# DEPLOY_DATE=$(date -d "$CREATED_AT" +%s) + +# # Calculate age in days for logging +# AGE_DAYS=$(( (CURRENT_DATE - DEPLOY_DATE) / 86400 )) + +# # Check if deployment is older than cutoff +# if [ "$DEPLOY_DATE" -lt "$CUTOFF_DATE" ]; then +# echo "Deleting deployment $DEPLOY_ID (created $CREATED_AT, $AGE_DAYS days old)" +# netlify api deleteSiteDeploy --data "{\"site_id\":\"${{ secrets.NETLIFY_SITE_ID }}\",\"deploy_id\":\"$DEPLOY_ID\"}" +# else +# echo "Keeping deployment $DEPLOY_ID (created $CREATED_AT, $AGE_DAYS days old)" +# fi +# done +# env: +# NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} +# NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}i ajshdkahsdkljhaksjh diff --git a/error.txt b/error.txt new file mode 100644 index 000000000..1e8a06f36 --- /dev/null +++ b/error.txt @@ -0,0 +1,6 @@ +Vercel CLI 41.4.1 +Retrieving project… +Deploying kitsunekode-manash/cms +Inspect: https://vercel.com/kitsunekode-manash/cms/14YnZJ5bPtdMcCtpBjZ9jwTBjH8o [3s] +Preview: https://cms-mfu4ij2mk-kitsunekode-manash.vercel.app [3s] +Queued diff --git a/package.json b/package.json index 6f5c62145..60a7985af 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "nextjs-toploader": "^1.6.11", "notion-client": "^6.16.0", "pdf-lib": "^1.17.1", + "prettier-plugin-tailwindcss": "^0.6.11", "prismjs": "^1.29.0", "qs": "^6.13.0", "react": "^18", @@ -106,8 +107,17 @@ "prisma": "^5.18.0", "tailwindcss": "^3.3.0", "ts-node": "^10.9.2", - "typescript": "^5.4.5", + "typescript": "^5.3.3", "vitest": "^1.6.0", "vitest-mock-extended": "^1.3.1" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@prisma/client", + "@prisma/engines", + "bcrypt", + "esbuild", + "prisma" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d6bb3dc4..6e97e270a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,6 +140,9 @@ importers: pdf-lib: specifier: ^1.17.1 version: 1.17.1 + prettier-plugin-tailwindcss: + specifier: ^0.6.11 + version: 0.6.11(prettier@3.3.3) prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -181,7 +184,7 @@ importers: version: 2.5.2 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.5.4))) + version: 1.0.7(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.3.3))) vaul: specifier: ^0.8.9 version: 0.8.9(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -227,10 +230,10 @@ importers: version: 18.3.0 '@typescript-eslint/eslint-plugin': specifier: ^6.20.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^6.20.0 - version: 6.21.0(eslint@8.57.0)(typescript@5.5.4) + version: 6.21.0(eslint@8.57.0)(typescript@5.3.3) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -245,19 +248,19 @@ importers: version: 5.18.0 tailwindcss: specifier: ^3.3.0 - version: 3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.5.4)) + version: 3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.3.3)) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.16.0)(typescript@5.5.4) + version: 10.9.2(@types/node@20.16.0)(typescript@5.3.3) typescript: - specifier: ^5.4.5 - version: 5.5.4 + specifier: ^5.3.3 + version: 5.3.3 vitest: specifier: ^1.6.0 version: 1.6.0(@types/node@20.16.0)(jsdom@24.1.1)(terser@5.31.6) vitest-mock-extended: specifier: ^1.3.1 - version: 1.3.2(typescript@5.5.4)(vitest@1.6.0(@types/node@20.16.0)(jsdom@24.1.1)(terser@5.31.6)) + version: 1.3.2(typescript@5.3.3)(vitest@1.6.0(@types/node@20.16.0)(jsdom@24.1.1)(terser@5.31.6)) packages: @@ -3415,6 +3418,61 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-plugin-tailwindcss@0.6.11: + resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + prettier@3.3.3: resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} @@ -4131,8 +4189,8 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true @@ -5592,13 +5650,13 @@ snapshots: '@types/warning@3.0.3': {} - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.6 eslint: 8.57.0 @@ -5606,22 +5664,22 @@ snapshots: ignore: 5.3.2 natural-compare: 1.4.0 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.3.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.3.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.6 eslint: 8.57.0 optionalDependencies: - typescript: 5.5.4 + typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -5630,21 +5688,21 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.3.3)': dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) debug: 4.3.6 eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.3.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.3.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@6.21.0': {} - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3)': dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 @@ -5653,20 +5711,20 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.3 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.3.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.3.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.3.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) eslint: 8.57.0 semver: 7.6.3 transitivePeerDependencies: @@ -7851,13 +7909,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.41 - postcss-load-config@4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.5.4)): + postcss-load-config@4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.3.3)): dependencies: lilconfig: 3.1.2 yaml: 2.5.0 optionalDependencies: postcss: 8.4.41 - ts-node: 10.9.2(@types/node@20.16.0)(typescript@5.5.4) + ts-node: 10.9.2(@types/node@20.16.0)(typescript@5.3.3) postcss-nested@6.2.0(postcss@8.4.41): dependencies: @@ -7892,6 +7950,10 @@ snapshots: prelude-ls@1.2.1: {} + prettier-plugin-tailwindcss@0.6.11(prettier@3.3.3): + dependencies: + prettier: 3.3.3 + prettier@3.3.3: {} pretty-format@27.5.1: @@ -8575,11 +8637,11 @@ snapshots: tailwind-merge@2.5.2: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.5.4))): + tailwindcss-animate@1.0.7(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.3.3))): dependencies: - tailwindcss: 3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.5.4)) + tailwindcss: 3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.3.3)) - tailwindcss@3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.5.4)): + tailwindcss@3.4.10(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.3.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -8598,7 +8660,7 @@ snapshots: postcss: 8.4.41 postcss-import: 15.1.0(postcss@8.4.41) postcss-js: 4.0.1(postcss@8.4.41) - postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.5.4)) + postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.16.0)(typescript@5.3.3)) postcss-nested: 6.2.0(postcss@8.4.41) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -8678,19 +8740,19 @@ snapshots: trough@2.2.0: {} - ts-api-utils@1.3.0(typescript@5.5.4): + ts-api-utils@1.3.0(typescript@5.3.3): dependencies: - typescript: 5.5.4 + typescript: 5.3.3 ts-easing@0.2.0: {} - ts-essentials@10.0.2(typescript@5.5.4): + ts-essentials@10.0.2(typescript@5.3.3): optionalDependencies: - typescript: 5.5.4 + typescript: 5.3.3 ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@20.16.0)(typescript@5.5.4): + ts-node@10.9.2(@types/node@20.16.0)(typescript@5.3.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -8704,7 +8766,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.5.4 + typescript: 5.3.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -8720,7 +8782,7 @@ snapshots: type-fest@0.20.2: {} - typescript@5.5.4: {} + typescript@5.3.3: {} ufo@1.5.4: {} @@ -8920,10 +8982,10 @@ snapshots: fsevents: 2.3.3 terser: 5.31.6 - vitest-mock-extended@1.3.2(typescript@5.5.4)(vitest@1.6.0(@types/node@20.16.0)(jsdom@24.1.1)(terser@5.31.6)): + vitest-mock-extended@1.3.2(typescript@5.3.3)(vitest@1.6.0(@types/node@20.16.0)(jsdom@24.1.1)(terser@5.31.6)): dependencies: - ts-essentials: 10.0.2(typescript@5.5.4) - typescript: 5.5.4 + ts-essentials: 10.0.2(typescript@5.3.3) + typescript: 5.3.3 vitest: 1.6.0(@types/node@20.16.0)(jsdom@24.1.1)(terser@5.31.6) vitest@1.6.0(@types/node@20.16.0)(jsdom@24.1.1)(terser@5.31.6): diff --git a/prisma/migrations/20250225124531_add_slides_type/migration.sql b/prisma/migrations/20250225124531_add_slides_type/migration.sql new file mode 100644 index 000000000..0c8282ed8 --- /dev/null +++ b/prisma/migrations/20250225124531_add_slides_type/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "SlidesType" AS ENUM ('NOTION', 'NOT_NOTION'); + +-- AlterTable +ALTER TABLE "VideoMetadata" ADD COLUMN "slidesType" "SlidesType" DEFAULT 'NOTION'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ca8e4431c..cc825a456 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -118,6 +118,7 @@ model VideoMetadata { segments Json? content Content @relation(fields: [contentId], references: [id]) slides String? // link to slides + slidesType SlidesType? @default(NOTION) thumbnail_mosiac_url String? duration Int? migration_status MigrationStatus @default(NOT_MIGRATED) @@ -361,3 +362,8 @@ enum MigrationStatus { MIGRATED MIGRATION_ERROR } + +enum SlidesType { + NOTION + NOT_NOTION +} diff --git a/prisma/seed.ts b/prisma/seed.ts index 0a40fbea3..7e2a61c6b 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -133,7 +133,25 @@ async function seedContent() { }, { type: 'video', - title: 'test video for week 1', + title: 'test video for week 1 - notion slides', + hidden: false, + thumbnail: + 'https://appx-recordings.s3.ap-south-1.amazonaws.com/drm/100x/images/week-1-orientation.jpg', + parentId: folderId, + commentsCount: 0, + }, + { + type: 'video', + title: 'test video 2 for week 1 - pdf slides', + hidden: false, + thumbnail: + 'https://appx-recordings.s3.ap-south-1.amazonaws.com/drm/100x/images/week-1-orientation.jpg', + parentId: folderId, + commentsCount: 0, + }, + { + type: 'video', + title: 'test video 2 for week 1 - projects.100xdevs.com slides', hidden: false, thumbnail: 'https://appx-recordings.s3.ap-south-1.amazonaws.com/drm/100x/images/week-1-orientation.jpg', @@ -179,12 +197,22 @@ async function seedNotionMetadata() { } } -async function seedVideoMetadata() { +async function seedVideoMetadata({ + slides, + id, + contentId, + slidesType +}: { + slides: string; + slidesType: 'NOTION' | 'NOT_NOTION'; + contentId: number; + id: number; +}) { try { await db.videoMetadata.create({ data: { - id: 1, - contentId: 3, + id, + contentId, video_1080p_mp4_1: 'https://www.w3schools.com/html/mov_bbb.mp4', video_1080p_mp4_2: 'https://www.w3schools.com/html/mov_bbb.mp4', video_1080p_mp4_3: 'https://www.w3schools.com/html/mov_bbb.mp4', @@ -209,13 +237,13 @@ async function seedVideoMetadata() { video_360p_2: 'https://www.w3schools.com/html/mov_bbb.mp4', video_360p_3: 'https://www.w3schools.com/html/mov_bbb.mp4', video_360p_4: 'https://www.w3schools.com/html/mov_bbb.mp4', - slides: - 'https://appx-recordings.s3.ap-south-1.amazonaws.com/drm/100x/slides/Loops%2C+callbacks.pdf', - segments: [ - { title: "Introduction", start: 0, end: 3 }, - { title: "Chapter 1", start: 3, end: 7 }, - { title: "Conclusion", start: 7, end: 10 } - ] + slides, + segments: [ + { title: 'Introduction', start: 0, end: 3 }, + { title: 'Chapter 1', start: 3, end: 7 }, + { title: 'Conclusion', start: 7, end: 10 }, + ], + slidesType, }, }); } catch (error) { @@ -294,7 +322,25 @@ async function seedDatabase() { await seedContent(); await seedCourseContent(); await seedNotionMetadata(); - await seedVideoMetadata(); + await seedVideoMetadata({ + id: 1, + contentId: 3, + slides: '39298af78c0f4c4ea780fd448551bad3', + slidesType: 'NOTION', + }); + await seedVideoMetadata({ + id: 2, + contentId: 4, + slides: + 'https://media.geeksforgeeks.org/wp-content/cdn-uploads/20210101201653/PDF.pdf', + slidesType: 'NOT_NOTION', + }); + await seedVideoMetadata({ + id: 3, + contentId: 5, + slides: 'https://projects.100xdevs.com', + slidesType: 'NOT_NOTION', + }); await seedPurchases(); await addClassesFromAugustToMay(); } catch (error) { diff --git a/src/app/(main)/(pages)/watch-history/page.tsx b/src/app/(main)/(pages)/watch-history/page.tsx index aba4163c2..32cc3676e 100644 --- a/src/app/(main)/(pages)/watch-history/page.tsx +++ b/src/app/(main)/(pages)/watch-history/page.tsx @@ -90,7 +90,7 @@ async function getWatchHistory() { updatedAt: 'desc', }, }); - +//@ts-ignore const filteruserVideoProgress: TWatchHistory[] = userVideoProgress .map((videoProgress) => { const filteredCourse = videoProgress?.content?.parent?.courses.filter( diff --git a/src/app/api/notion/route.ts b/src/app/api/notion/route.ts index 0e4dddde6..80903acc6 100644 --- a/src/app/api/notion/route.ts +++ b/src/app/api/notion/route.ts @@ -8,6 +8,17 @@ export async function GET(req: NextRequest) { const searchParams = new URLSearchParams(url.search); // @ts-ignore const contentId: number = parseInt(searchParams.get('id'), 10); + + // @ts-ignore + const notionIdFromParams: string = searchParams.get('notionId')?.toString(); + + if (notionIdFromParams) { + const recordMap = await notion.getPage(notionIdFromParams); + return NextResponse.json({ + recordMap, + }); + } + const notionMetadata = await db.notionMetadata.findFirst({ where: { contentId, diff --git a/src/components/FilterContent.tsx b/src/components/FilterContent.tsx index 8f3917ef4..1fbddb7c9 100644 --- a/src/components/FilterContent.tsx +++ b/src/components/FilterContent.tsx @@ -4,7 +4,7 @@ import { Check, ChevronsUpDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { useRecoilState } from 'recoil'; -import { selectFilter } from '@/store/atoms/filterContent'; +import { FilterType, selectFilter } from '@/store/atoms/filterContent'; import { Command, CommandGroup, @@ -59,7 +59,7 @@ export const FilterContent = forwardRef( value={filters.value} className={`px-4 ${props.className || ''}`} onSelect={(currentValue) => { - setValue(currentValue === value ? '' : currentValue); + setValue(currentValue === value ? 'all' as FilterType : currentValue as FilterType); setOpen(false); }} > diff --git a/src/components/NotionRenderer.tsx b/src/components/NotionRenderer.tsx index bed2adad1..b5d0370be 100644 --- a/src/components/NotionRenderer.tsx +++ b/src/components/NotionRenderer.tsx @@ -29,15 +29,17 @@ import { handleMarkAsCompleted } from '@/lib/utils'; export const NotionRenderer = ({ id, courseId, + notionId, }: { id: string; - courseId: number; + courseId?: number; + notionId?: string; }) => { const { resolvedTheme } = useTheme(); const [data, setData] = useState(null); async function main() { - const res = await fetch(`/api/notion?id=${id}`); + const res = await fetch(`/api/notion?id=${id}¬ionId=${notionId}`); const json = await res.json(); setData(json.recordMap); } @@ -46,7 +48,9 @@ export const NotionRenderer = ({ main(); return () => { - handleMarkAsCompleted(true, courseId); + if (courseId) { + handleMarkAsCompleted(true, courseId); + } }; }, [id]); diff --git a/src/components/ResizeBar.tsx b/src/components/ResizeBar.tsx new file mode 100644 index 000000000..731102bbe --- /dev/null +++ b/src/components/ResizeBar.tsx @@ -0,0 +1,78 @@ +'use client'; +import React, { useRef, useState } from 'react'; + +interface ResizeBarProps { + isResized: React.MutableRefObject; +} +const ResizeBar = ({ isResized }: ResizeBarProps) => { + const [mouseY, setMouseY] = useState(null); + const handleRef = useRef(null); + + const handleMouseDown = (e: React.MouseEvent) => { + isResized.current = true; + e.preventDefault(); // Prevent text selection during drag + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (handleRef.current) { + const rect = handleRef.current.getBoundingClientRect(); + setMouseY(e.clientY - rect.top); + } + }; + + const handleMouseLeave = () => { + setMouseY(null); + }; + const handleMouseUp = () => { + isResized.current = false; + }; + + return ( +
+ {/* Resize handle */} +
+ {/* Vertical dots pattern - centered */} +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ + {/* Localized glow effect that follows mouse */} + {mouseY !== null && ( +
+ )} + + {/* Vertical borders */} +
+
+
+
+ ); +}; + +export default ResizeBar; diff --git a/src/components/VideoPlayer2.tsx b/src/components/VideoPlayer2.tsx index 6e317c1ca..f038e6710 100644 --- a/src/components/VideoPlayer2.tsx +++ b/src/components/VideoPlayer2.tsx @@ -18,6 +18,8 @@ import { toast } from 'sonner'; import { createRoot } from 'react-dom/client'; import { PictureInPicture2 } from 'lucide-react'; import { AppxVideoPlayer } from './AppxVideoPlayer'; +import { useRecoilState } from 'recoil'; +import { pipTrigger } from '@/store/atoms/trigger'; // todo correct types interface VideoPlayerProps { @@ -61,6 +63,7 @@ export const VideoPlayer: FunctionComponent = ({ const [player, setPlayer] = useState(null); const searchParams = useSearchParams(); const vidUrl = options.sources[0].src; + const [pip_Trigger, setPip_Trigger] = useRecoilState(pipTrigger); const togglePictureInPicture = async () => { try { @@ -77,6 +80,7 @@ export const VideoPlayer: FunctionComponent = ({ error.name !== 'NotSupportedError' ) { console.error('Failed to toggle Picture-in-Picture mode:', error); + setPip_Trigger(false); toast.error('Failed to toggle Picture-in-Picture mode.'); } } @@ -111,7 +115,6 @@ export const VideoPlayer: FunctionComponent = ({ }; const setupZoomFeatures = (player: any) => { - if (typeof window === 'undefined' || typeof document === 'undefined') return; const videoEl = player.el().querySelector('video'); @@ -166,7 +169,8 @@ export const VideoPlayer: FunctionComponent = ({ })(); // Unified gesture handler - const handleGestureControl = (e: HammerInput) => { + //@ts-ignore + const handleGestureControl = (e) => { const target = e.srcEvent.target as HTMLElement; const isControlBar = target.closest('.vjs-control-bar'); @@ -360,6 +364,19 @@ export const VideoPlayer: FunctionComponent = ({ } }, [contentId, player]); + useEffect(() => { + if (pip_Trigger) { + togglePictureInPicture(); + } + const handleLeavePictureInPicture = () => { + setPip_Trigger(false); + }; + document.addEventListener('leavepictureinpicture', handleLeavePictureInPicture); + return () => { + document.removeEventListener('leavepictureinpicture', handleLeavePictureInPicture); + }; + }, [pip_Trigger]); + useEffect(() => { if (!player) { return; diff --git a/src/components/admin/ContentRenderer.tsx b/src/components/admin/ContentRenderer.tsx index eb5b1663e..516b3f5e9 100644 --- a/src/components/admin/ContentRenderer.tsx +++ b/src/components/admin/ContentRenderer.tsx @@ -88,6 +88,7 @@ export const getMetadata = async (contentId: number) => { segments: metadata['segments'], thumbnails: metadata['thumbnail_mosiac_url'], appxVideoJSON: metadata['appxVideoJSON'], + slidesType: metadata['slidesType'], }; if (user?.bunnyProxyEnabled) { @@ -106,6 +107,7 @@ export const getMetadata = async (contentId: number) => { segments: metadata['segments'], thumbnails: metadata['thumbnail_mosiac_url'], appxVideoJSON: metadata['appxVideoJSON'], + slidesType: metadata['slidesType'], }; const isHighestQualityUrlAccessible = await isUrlAccessible(mainUrls['1080']); diff --git a/src/components/admin/ContentRendererClient.tsx b/src/components/admin/ContentRendererClient.tsx index f73c0422e..551ca2662 100644 --- a/src/components/admin/ContentRendererClient.tsx +++ b/src/components/admin/ContentRendererClient.tsx @@ -1,11 +1,15 @@ 'use client'; -import { useSearchParams, useRouter } from 'next/navigation'; import { VideoPlayerSegment } from '@/components/VideoPlayerSegment'; -import VideoContentChapters from '../VideoContentChapters'; import { ChevronDown, ChevronUp } from 'lucide-react'; -import { useMemo, useState } from 'react'; -import { Button } from '../ui/button'; import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { pipTrigger } from '@/store/atoms/trigger'; +import { NotionRenderer } from '@/components/NotionRenderer'; +import ResizeBar from '@/components/ResizeBar'; +import { Button } from '@/components/ui/button'; +import VideoContentChapters from '@/components/VideoContentChapters'; export const ContentRendererClient = ({ metadata, @@ -32,10 +36,18 @@ export const ContentRendererClient = ({ const [showChapters, setShowChapters] = useState( metadata?.segments?.length > 0, ); + + const setPipTrigger = useSetRecoilState(pipTrigger); + + const [isDualView, setIsDualView] = useState(false); + const searchParams = useSearchParams(); + const screenWidth = useRef(1000); const router = useRouter(); + const [width, setWidth] = useState(screenWidth.current * 0.5); + //@ts-ignore const [quality, setQuality] = useState( searchParams.get('quality') ?? '1080', @@ -73,46 +85,181 @@ export const ContentRendererClient = ({ setShowChapters((prev) => !prev); }; + const isResized = useRef(false); + + useEffect(() => { + screenWidth.current = window.innerWidth; + const minWidth = screenWidth.current * 0.1; // 10% of screen width + const maxWidth = screenWidth.current * 0.65; // % of screen width + + // window.addEventListener('touchstart', (e) => { + // if (!isResized.current) { + // return; + // } + + // setWidth((previousWidth) => { + // const newWidth = previousWidth + e.touches[0].clientX; + + // if (newWidth <= minWidth) { + // setPipTrigger(true); + // return minWidth; + // } else if (newWidth >=maxWidth) { + // setIsDualView(false); + // isResized.current=false; + // } + + // const isWidthInRange = newWidth >= minWidth && newWidth <= maxWidth; + + // return isWidthInRange ? newWidth : previousWidth; + // }); + // }); + const handleMouseMove = (e: MouseEvent) => { + if (!isResized.current) { + return; + } + + if (e.buttons === 0) { + isResized.current = false; + return; + } + + setWidth((previousWidth) => { + const newWidth = previousWidth + e.movementX; + + if (newWidth < minWidth) { + setPipTrigger(true); + return minWidth + 25; + } else if (newWidth >= maxWidth) { + setIsDualView(false); + isResized.current = false; + } + + const isWidthInRange = newWidth >= minWidth && newWidth <= maxWidth; + + return isWidthInRange ? newWidth : previousWidth; + }); + }; + + const handleMouseUp = () => { + isResized.current = false; + }; + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + //cleanups + }; + }, []); + return (
- { }} - /> +
+
+
+ {}} + /> +
+ + {isDualView && } +
+ {isDualView && ( +
+ {metadata.slidesType === 'NOT_NOTION' && ( + + )} + + {metadata.slidesType === 'NOTION' && ( + + )} +
+ )} +

{content.title}

{metadata.slides ? ( - - - +
+ + + + + {!isDualView ? ( + <> + {metadata.slidesType === 'NOT_NOTION' ? ( + + + + ) : ( + + )} + + ) : ( + + )} +
) : null}
diff --git a/src/components/admin/UpdateVideoClient.tsx b/src/components/admin/UpdateVideoClient.tsx index 9d8046a4d..6fc516a22 100644 --- a/src/components/admin/UpdateVideoClient.tsx +++ b/src/components/admin/UpdateVideoClient.tsx @@ -12,6 +12,7 @@ import { Input } from '../ui/input'; import { Button } from '../ui/button'; import { Label } from '../ui/label'; import { toast } from 'sonner'; +import { RadioGroup, RadioGroupItem } from '../ui/radio-group'; export const UpdateVideoClient = ({ content, @@ -28,6 +29,7 @@ export const UpdateVideoClient = ({ const [link1080, setLink1080] = useState(''); const [link720, setLink720] = useState(''); const [link360, setLink360] = useState(''); + const [slidesType, setSlidesType] = useState('NOTION'); const [link1080_mp4, setLink1080_mp4] = useState(''); const [link720_mp4, setLink720_mp4] = useState(''); @@ -183,7 +185,20 @@ export const UpdateVideoClient = ({ type="text" placeholder="Admin password" onChange={(e) => setAdminPassword(e.target.value)} - /> + /> + Slides Type + + setSlidesType(value)}> +
+ + +
+
+ + +
+
+