├── .cz-config.js ├── .dockerignore ├── .editorconfig ├── .env.example ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── enhancement.yml └── workflows │ ├── ci.yml │ ├── pre-release.yml │ ├── release.yml │ ├── stale.yml │ └── tests.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── commitlint.config.ts ├── config └── .gitkeep ├── docker-compose.yml ├── eslint.config.js ├── lint-staged.config.js ├── messages ├── de.json ├── en.json ├── et.json └── fr.json ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── browserconfig.xml ├── clouds.png ├── icon-192x192.png ├── icon-512x512.png ├── manifest.json ├── mstile-150x150.png └── twinkling.png ├── release.config.js ├── src ├── app │ ├── _assets │ │ └── stars.png │ ├── _components │ │ ├── AppProvider.tsx │ │ ├── CardWrapper.tsx │ │ ├── GlobalContextProvider.tsx │ │ ├── GoogleAnalytics.tsx │ │ ├── Home.tsx │ │ ├── LocaleSelect.tsx │ │ ├── PageTitle.tsx │ │ └── SessionProvider.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── image │ │ │ └── route.ts │ │ ├── missing-setting │ │ │ └── route.ts │ │ └── status │ │ │ └── route.ts │ ├── apple-icon.png │ ├── dashboard │ │ ├── [slug] │ │ │ └── page.tsx │ │ ├── _components │ │ │ ├── Dashboard.tsx │ │ │ ├── DashboardFilters.tsx │ │ │ ├── DashboardLoader.tsx │ │ │ ├── DashboardNav.tsx │ │ │ ├── DashboardNavContent.tsx │ │ │ ├── PeriodSelect.tsx │ │ │ └── PeriodSelectContent.tsx │ │ ├── error.tsx │ │ ├── layout.tsx │ │ └── users │ │ │ └── page.tsx │ ├── favicon.ico │ ├── icon.svg │ ├── layout.tsx │ ├── not-found.tsx │ ├── opengraph-image.jpg │ ├── page.tsx │ ├── rewind │ │ ├── _components │ │ │ ├── RewindStat.tsx │ │ │ ├── RewindStories.tsx │ │ │ ├── StatListItem.tsx │ │ │ ├── Stories │ │ │ │ ├── Audio.tsx │ │ │ │ ├── AudioTop.tsx │ │ │ │ ├── Goodbye.tsx │ │ │ │ ├── Libraries.tsx │ │ │ │ ├── Movies.tsx │ │ │ │ ├── MoviesTop.tsx │ │ │ │ ├── Requests.tsx │ │ │ │ ├── Shows.tsx │ │ │ │ ├── ShowsTop.tsx │ │ │ │ ├── Total.tsx │ │ │ │ ├── UsersTop.tsx │ │ │ │ └── Welcome.tsx │ │ │ ├── StoryWrapper.tsx │ │ │ └── UserSelect.tsx │ │ ├── error.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── settings │ │ ├── _actions │ │ │ └── updateSettings.ts │ │ ├── _components │ │ │ ├── SettingsForm.tsx │ │ │ ├── SettingsNav.tsx │ │ │ └── SettingsSaveButton.tsx │ │ ├── connection │ │ │ ├── _actions │ │ │ │ └── updateConnectionSettings.ts │ │ │ ├── _components │ │ │ │ └── ConnectionSettingsForm.tsx │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── _actions │ │ │ │ └── updateDashboardSettings.ts │ │ │ ├── _components │ │ │ │ └── DashboardSettingsForm.tsx │ │ │ └── page.tsx │ │ ├── general │ │ │ ├── _actions │ │ │ │ └── updateGeneralSettings.ts │ │ │ ├── _components │ │ │ │ └── GeneralSettingsForm.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── rewind │ │ │ ├── _actions │ │ │ └── updateRewindSettings.ts │ │ │ ├── _components │ │ │ └── RewindSettingsForm.tsx │ │ │ └── page.tsx │ └── ~offline │ │ └── page.tsx ├── assets │ ├── github.svg │ └── plex.svg ├── components │ ├── DatePicker.tsx │ ├── Loader.tsx │ └── MediaItem │ │ ├── MediaItem.tsx │ │ ├── MediaItemTitle.tsx │ │ ├── MediaItems.tsx │ │ ├── PlexDeeplink.tsx │ │ ├── anonymous.svg │ │ └── placeholder.svg ├── hooks │ ├── usePlexAuth.ts │ └── useTimer.ts ├── i18n │ ├── locale.ts │ └── request.ts ├── lib │ ├── auth.ts │ └── sw.ts ├── middleware.ts ├── styles │ └── globals.css ├── types │ ├── custom.d.ts │ ├── dashboard.d.ts │ ├── index.d.ts │ ├── next-auth.d.ts │ ├── rewind.d.ts │ ├── settings.d.ts │ └── tautulli.d.ts └── utils │ ├── constants.ts │ ├── fetchOverseerr.ts │ ├── fetchTautulli.ts │ ├── fetchTmdb.ts │ ├── formatting.ts │ ├── getDashboard.ts │ ├── getMediaAdditionalData.ts │ ├── getPeriod.ts │ ├── getRewind.ts │ ├── getSettings.ts │ ├── getUsersTop.ts │ ├── getVersion.ts │ ├── helpers.ts │ └── motion.ts ├── stylelint.config.js └── tsconfig.json /.cz-config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | types: [ 3 | { value: 'feat', name: 'feat: A new feature' }, 4 | { value: 'fix', name: 'fix: A bug fix' }, 5 | { value: 'ui', name: 'ui: Changes related to the UI' }, 6 | { 7 | value: 'perf', 8 | name: 'perf: A code change that improves performance', 9 | }, 10 | { value: 'docs', name: 'docs: Documentation only changes' }, 11 | { value: 'revert', name: 'revert: Revert a previous commit' }, 12 | { 13 | value: 'chore', 14 | name: 'chore: Changes to auxiliary tools such as libraries and dependencies', 15 | }, 16 | { 17 | value: 'refactor', 18 | name: 'refactor: A code change that neither fixes a bug nor adds a feature', 19 | }, 20 | { 21 | value: 'build', 22 | name: 'build: Changes that affect the build system or external dependencies\n (example scopes: docker, pnpm)', 23 | }, 24 | { 25 | value: 'ci', 26 | name: 'ci: Changes to our CI configuration files and scripts', 27 | }, 28 | { 29 | value: 'style', 30 | name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)', 31 | }, 32 | { 33 | value: 'test', 34 | name: 'test: Adding missing tests or correcting existing tests', 35 | }, 36 | ], 37 | scopes: [ 38 | { name: 'rewind' }, 39 | { name: 'dashboard' }, 40 | { name: 'config' }, 41 | { name: 'api' }, 42 | ], 43 | } 44 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | .git 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_SECRET= 2 | NEXTAUTH_URL=http://localhost:3000 3 | NEXT_PUBLIC_SITE_URL=http://localhost:3000 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Global code ownership 2 | * @RaunoT 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [RaunoT] 4 | patreon: PlexRewind 5 | custom: ['paypal.me/raunot'] 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Report a problem 3 | labels: ['type:bug', 'awaiting triage'] 4 | body: 5 | - type: input 6 | id: version 7 | attributes: 8 | label: Plex Rewind Version 9 | description: What version of Plex Rewind are you running? You can find this at the bottom of the settings page. 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: description 14 | attributes: 15 | label: Description 16 | description: Please provide a clear and concise description of the bug or issue. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: repro-steps 21 | attributes: 22 | label: Steps to reproduce 23 | description: Please tell us how we can reproduce the undesired behavior. 24 | placeholder: | 25 | 1. Go to... 26 | 2. Click on... 27 | 3. See error in... 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: screenshots 32 | attributes: 33 | label: Screenshots 34 | description: If applicable, please provide screenshots depicting the problem. 35 | - type: textarea 36 | id: logs 37 | attributes: 38 | label: Logs 39 | description: Please copy and paste any relevant log output. 40 | render: shell 41 | - type: dropdown 42 | id: platform 43 | attributes: 44 | label: Platform 45 | options: 46 | - desktop 47 | - smartphone 48 | - tablet 49 | validations: 50 | required: true 51 | - type: input 52 | id: device 53 | attributes: 54 | label: Device 55 | description: For example - iPhone 15 Pro, Samsung Galaxy S22 56 | validations: 57 | required: true 58 | - type: input 59 | id: os 60 | attributes: 61 | label: Operating System 62 | description: For example - iOS 17.1, Windows 11, Android 14.0 63 | validations: 64 | required: true 65 | - type: input 66 | id: browser 67 | attributes: 68 | label: Browser and version 69 | description: For example - Chrome 120.0.6099.129, Safari 17.2, Firefox 121.0 70 | validations: 71 | required: true 72 | - type: textarea 73 | id: additional-context 74 | attributes: 75 | label: Additional context 76 | description: Any other relevant information about the issue. 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature request 2 | description: Suggest an enhancement, improvement or idea 3 | labels: ['type:enhancement', 'awaiting triage'] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Description 9 | description: What is the motivation or use case for this enhancement? 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: desired-behavior 14 | attributes: 15 | label: Desired behavior 16 | description: How would the feature work and what would it look like? 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: additional-context 21 | attributes: 22 | label: Additional context 23 | description: Any other relevant information about the feature request. 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | ci: 8 | name: Build and publish test image 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v3 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | 22 | - name: Log in to GitHub Container Registry 23 | uses: docker/login-action@v3 24 | with: 25 | registry: ghcr.io 26 | username: ${{ github.repository_owner }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Build and push Docker image 30 | uses: docker/build-push-action@v6 31 | with: 32 | context: . 33 | push: true 34 | platforms: linux/amd64,linux/arm64,linux/arm/v7 35 | tags: | 36 | ghcr.io/raunot/plex-rewind:${{ github.sha }} 37 | build-args: | 38 | NEXT_PUBLIC_VERSION_TAG=${{ github.sha }} 39 | cache-from: type=gha 40 | cache-to: type=gha,mode=max 41 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Pre-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | pre-release: 10 | name: Publish image and pre-release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup pnpm 19 | uses: pnpm/action-setup@v4 20 | with: 21 | version: 10 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 22 27 | cache: 'pnpm' 28 | 29 | - name: Install dependencies 30 | run: pnpm install 31 | 32 | - name: Run Semantic Release dry run 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: npx semantic-release --dry-run 36 | 37 | - name: Set up QEMU 38 | if: env.NEXT_VERSION_TAG 39 | uses: docker/setup-qemu-action@v3 40 | 41 | - name: Set up Docker Buildx 42 | if: env.NEXT_VERSION_TAG 43 | uses: docker/setup-buildx-action@v3 44 | 45 | - name: Log in to GitHub Container Registry 46 | if: env.NEXT_VERSION_TAG 47 | uses: docker/login-action@v3 48 | with: 49 | registry: ghcr.io 50 | username: ${{ github.repository_owner }} 51 | password: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Build and push Docker image 54 | if: env.NEXT_VERSION_TAG 55 | uses: docker/build-push-action@v6 56 | with: 57 | context: . 58 | push: true 59 | platforms: linux/amd64,linux/arm64,linux/arm/v7 60 | tags: | 61 | ghcr.io/raunot/plex-rewind:develop 62 | ghcr.io/raunot/plex-rewind:${{ env.NEXT_VERSION_TAG }} 63 | build-args: | 64 | NEXT_PUBLIC_VERSION_TAG=${{ env.NEXT_VERSION_TAG }} 65 | cache-from: type=gha 66 | cache-to: type=gha,mode=max 67 | 68 | - name: Run Semantic Release publish 69 | if: env.NEXT_VERSION_TAG 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | run: npx semantic-release 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Publish image and release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | persist-credentials: false 18 | 19 | - name: Setup pnpm 20 | uses: pnpm/action-setup@v4 21 | with: 22 | version: 10 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 22 28 | cache: 'pnpm' 29 | 30 | - name: Install dependencies 31 | run: pnpm install 32 | 33 | - name: Run Semantic Release dry run 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | run: npx semantic-release --dry-run 37 | 38 | - name: Set up QEMU 39 | if: env.NEXT_VERSION_TAG 40 | uses: docker/setup-qemu-action@v3 41 | 42 | - name: Set up Docker Buildx 43 | if: env.NEXT_VERSION_TAG 44 | uses: docker/setup-buildx-action@v3 45 | 46 | - name: Log in to GitHub Container Registry 47 | if: env.NEXT_VERSION_TAG 48 | uses: docker/login-action@v3 49 | with: 50 | registry: ghcr.io 51 | username: ${{ github.repository_owner }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: Build and push Docker image 55 | if: env.NEXT_VERSION_TAG 56 | uses: docker/build-push-action@v6 57 | with: 58 | context: . 59 | push: true 60 | platforms: linux/amd64,linux/arm64,linux/arm/v7 61 | tags: | 62 | ghcr.io/raunot/plex-rewind:latest 63 | ghcr.io/raunot/plex-rewind:${{ env.NEXT_VERSION_TAG }} 64 | build-args: | 65 | NEXT_PUBLIC_VERSION_TAG=${{ env.NEXT_VERSION_TAG }} 66 | cache-from: type=gha 67 | cache-to: type=gha,mode=max 68 | 69 | - name: Run Semantic Release publish 70 | if: env.NEXT_VERSION_TAG 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 73 | run: npx semantic-release 74 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Check for stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 13 | exempt-issue-labels: 'never-stale' 14 | stale-issue-label: 'stale' 15 | only-issue-labels: 'awaiting triage' 16 | stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 17 | exempt-pr-labels: 'never-stale' 18 | stale-pr-label: 'stale' 19 | days-before-stale: 30 20 | days-before-close: 5 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | test: 10 | if: ${{ !(github.base_ref == 'main' && github.head_ref == 'develop') }} 11 | name: Lint & test build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup pnpm 18 | uses: pnpm/action-setup@v4 19 | with: 20 | version: 10 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 22 26 | cache: 'pnpm' 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | 31 | - name: Lint code and check formatting 32 | run: pnpm lint:all 33 | 34 | - name: Build 35 | run: pnpm build 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # cache 38 | .eslintcache 39 | .stylelintcache 40 | 41 | # pwa 42 | public/sw.js 43 | public/swe-worker-*.js 44 | 45 | #config 46 | config/settings.json 47 | config/settings-backup.json 48 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit "$1" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | pnpm-lock.yaml 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "editorconfig.editorconfig", 7 | "stylelint.vscode-stylelint" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": "never" 5 | }, 6 | "eslint.enable": true, 7 | "eslint.validate": [ 8 | "javascript", 9 | "javascriptreact", 10 | "typescript", 11 | "typescriptreact" 12 | ], 13 | "[css]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[javascript]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[typescript]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "css.validate": false, 23 | "stylelint.packageManager": "pnpm", 24 | "stylelint.snippet": ["css", "postcss"], 25 | "stylelint.validate": ["css", "postcss"], 26 | "prettier.documentSelectors": ["**/*.svg"], 27 | "files.associations": { 28 | "*.css": "tailwindcss" 29 | }, 30 | "typescript.tsdk": "node_modules/typescript/lib", 31 | "i18n-ally.localesPaths": ["messages", "src/i18n"], 32 | "i18n-ally.keystyle": "nested" 33 | } 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Plex Rewind 2 | 3 | Any contributions are welcome and greatly appreciated! 4 | 5 | Please follow the guidelines below. 6 | 7 | ## Development 8 | 9 | ### Tools 10 | 11 | - [Git](https://git-scm.com) for source control 12 | - [NodeJS](https://nodejs.org) (22.x or higher) 13 | - [VSCode](https://code.visualstudio.com) is highly recommended as an editor 14 | 15 | > Upon opening the project, a few extensions will be automatically recommended for install. These are highly recommended to install and will make development easier. You can find the list in `.vscode/extensions.json`. 16 | 17 | - [pnpm](https://pnpm.io) as a package manager 18 | 19 | ### Getting Started 20 | 21 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) the repository to your own GitHub account and [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device: 22 | 23 | ```bash 24 | git clone https://github.com/YOUR_USERNAME/plex-rewind.git 25 | cd plex-rewind 26 | ``` 27 | 28 | 2. Create `.env.local` file in the root of the project from the `.env.example` file: 29 | 30 | ```bash 31 | cp .env.example .env.local 32 | ``` 33 | 34 | 3. Install the project dependencies: 35 | 36 | ```bash 37 | pnpm i 38 | ``` 39 | 40 | 4. Create a new branch: 41 | 42 | ```bash 43 | git checkout -b develop 44 | ``` 45 | 46 | > You can read about how to name your branch under the [code guidelines](#code-guidelines). 47 | 48 | 5. Run the development environment: 49 | 50 | ```bash 51 | pnpm dev 52 | ``` 53 | 54 | 6. Create your patch and test your changes. Be sure to follow the [code](#code-guidelines) guidelines. 55 | 56 | 7. Should you need to update your fork, you can do so by rebasing from `upstream`: 57 | 58 | ```bash 59 | git fetch upstream 60 | git rebase upstream/develop 61 | git push origin BRANCH_NAME -f 62 | ``` 63 | 64 | ### Code guidelines 65 | 66 | - If you are taking on an existing bug or feature ticket, please comment on the [issue](https://github.com/RaunoT/plex-rewind/issues) to avoid multiple people working on the same thing. 67 | 68 | - All commits must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) rules. 69 | 70 | > Please make meaningful commits, or squash them prior to opening a pull request. 71 | 72 | - Do your best to check for spelling errors and grammatical mistakes. 73 | 74 | - Make sure to test your changes on different screen sizes to ensure responsiveness. 75 | 76 | - Do your research and follow the best practices for whatever tool or library you're using. 77 | 78 | - Use the appropriate Unicode characters for ellipses, arrows, and other special characters/symbols 79 | 80 | - Branch and PR naming should follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) logic. 81 | 82 | > Example branch name: `feat(dashboard)/new-feature` or `fix/bug` 83 | 84 | > Example pull request name: `feat(dashboard): new feature` or `fix: bug` 85 | 86 | - Make sure to keep your branch up-to-date. 87 | 88 | - Only open pull requests to `develop`, never `main`. 89 | 90 | Any pull requests opened to `main` will be closed. 91 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-slim AS base 2 | 3 | # Install dependencies only when needed 4 | FROM base AS deps 5 | WORKDIR /app 6 | 7 | # Install dependencies based on the preferred package manager 8 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ 9 | RUN \ 10 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 11 | elif [ -f package-lock.json ]; then npm ci; \ 12 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ 13 | else echo "Lockfile not found." && exit 1; \ 14 | fi 15 | 16 | # Rebuild the source code only when needed 17 | FROM base AS builder 18 | WORKDIR /app 19 | COPY --from=deps /app/node_modules ./node_modules 20 | COPY . . 21 | 22 | # Next.js collects completely anonymous telemetry data about general usage. 23 | # Learn more here: https://nextjs.org/telemetry 24 | # Uncomment the following line in case you want to disable telemetry during the build. 25 | ENV NEXT_TELEMETRY_DISABLED=1 26 | 27 | RUN \ 28 | if [ -f yarn.lock ]; then yarn run build; \ 29 | elif [ -f package-lock.json ]; then npm run build; \ 30 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ 31 | else echo "Lockfile not found." && exit 1; \ 32 | fi 33 | 34 | # Production image, copy all the files and run next 35 | FROM base AS runner 36 | WORKDIR /app 37 | 38 | # Install openssl in the runner stage 39 | RUN apt-get update && apt-get install -y --no-install-recommends openssl 40 | 41 | ENV NODE_ENV=production 42 | ENV BASE_DIR=/app 43 | ARG NEXT_PUBLIC_VERSION_TAG 44 | ENV NEXT_PUBLIC_VERSION_TAG=${NEXT_PUBLIC_VERSION_TAG} 45 | # Uncomment the following line in case you want to disable telemetry during runtime. 46 | ENV NEXT_TELEMETRY_DISABLED=1 47 | 48 | COPY --from=builder /app/public ./public 49 | COPY --from=builder /app/config ./config 50 | 51 | # Set the correct permission for prerender cache and config 52 | RUN mkdir .next 53 | 54 | # Automatically leverage output traces to reduce image size 55 | # https://nextjs.org/docs/advanced-features/output-file-tracing 56 | COPY --from=builder /app/.next/standalone ./ 57 | COPY --from=builder /app/.next/static ./.next/static 58 | 59 | EXPOSE 8383 60 | 61 | ENV PORT=8383 62 | 63 | # server.js is created by next build from the standalone output 64 | # https://nextjs.org/docs/pages/api-reference/config/next-config-js/output 65 | ENV HOSTNAME="0.0.0.0" 66 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from '@commitlint/types' 2 | 3 | const config: UserConfig = { 4 | extends: ['@commitlint/config-conventional'], 5 | rules: { 6 | 'type-enum': [ 7 | 2, 8 | 'always', 9 | [ 10 | 'feat', 11 | 'fix', 12 | 'ui', 13 | 'perf', 14 | 'docs', 15 | 'revert', 16 | 'chore', 17 | 'refactor', 18 | 'build', 19 | 'ci', 20 | 'style', 21 | 'test', 22 | ], 23 | ], 24 | }, 25 | } 26 | 27 | export default config 28 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaunoT/plex-rewind/6688f60565d2e2ee4f30438453b9950b07790f87/config/.gitkeep -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | plex-rewind: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | restart: unless-stopped 7 | ports: 8 | - 3000:8383 9 | env_file: '.env.local' 10 | volumes: 11 | - ./config:/app/config 12 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc' 2 | 3 | const compat = new FlatCompat({ 4 | baseDirectory: import.meta.dirname, 5 | recommendedConfig: {}, 6 | }) 7 | const eslintConfig = [ 8 | ...compat.config({ 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'next/core-web-vitals', 13 | 'prettier', 14 | ], 15 | parser: '@typescript-eslint/parser', 16 | plugins: ['@typescript-eslint', 'react-compiler', '@stylistic'], 17 | rules: { 18 | '@stylistic/padding-line-between-statements': [ 19 | 'error', 20 | { blankLine: 'always', prev: 'return', next: '*' }, 21 | { blankLine: 'always', prev: '*', next: 'return' }, 22 | { blankLine: 'always', prev: 'if', next: '*' }, 23 | { blankLine: 'always', prev: '*', next: 'if' }, 24 | { blankLine: 'always', prev: ['const', 'let'], next: '*' }, 25 | { blankLine: 'always', prev: '*', next: ['const', 'let'] }, 26 | { blankLine: 'never', prev: 'const', next: 'const' }, 27 | { blankLine: 'never', prev: 'let', next: 'let' }, 28 | ], 29 | 'react-compiler/react-compiler': 2, 30 | 'react/jsx-no-literals': 1, 31 | }, 32 | }), 33 | ] 34 | 35 | export default eslintConfig 36 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '**/*.{js,jsx,ts,tsx}': [ 3 | 'prettier --write --cache', 4 | 'eslint --cache --fix', 5 | 'bash -c tsc --noEmit --skipLibCheck', 6 | ], 7 | '**/*.css': 'stylelint --cache --fix', 8 | '**/*.{css,md,json}': 'prettier --write --cache', 9 | } 10 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import withSerwistInit from '@serwist/next' 2 | import { NextConfig } from 'next' 3 | import createNextIntlPlugin from 'next-intl/plugin' 4 | 5 | const isDev = process.env.NODE_ENV !== 'production' 6 | const nextConfig: NextConfig = { 7 | reactStrictMode: true, 8 | output: 'standalone', 9 | images: { 10 | remotePatterns: [ 11 | { 12 | protocol: 'http', 13 | hostname: 'localhost', 14 | }, 15 | { 16 | protocol: 'https', 17 | hostname: 'plex.tv', 18 | }, 19 | { 20 | protocol: 'https', 21 | hostname: 'image.tmdb.org', 22 | }, 23 | ], 24 | }, 25 | // logging: { 26 | // fetches: { 27 | // fullUrl: true, 28 | // }, 29 | // }, 30 | async headers() { 31 | return [ 32 | { 33 | source: '/(.*)', 34 | headers: [ 35 | { 36 | key: 'Strict-Transport-Security', 37 | value: 'max-age=63072000; includeSubDomains; preload', 38 | }, 39 | { 40 | key: 'X-Frame-Options', 41 | value: 'SAMEORIGIN', 42 | }, 43 | { 44 | key: 'X-Content-Type-Options', 45 | value: 'nosniff', 46 | }, 47 | { 48 | key: 'Referrer-Policy', 49 | value: 'strict-origin-when-cross-origin', 50 | }, 51 | { 52 | key: 'Permissions-Policy', 53 | value: '', 54 | }, 55 | ], 56 | }, 57 | ] 58 | }, 59 | } 60 | const revision = crypto.randomUUID() 61 | const withSerwist = withSerwistInit({ 62 | cacheOnNavigation: true, 63 | swSrc: 'src/lib/sw.ts', 64 | swDest: 'public/sw.js', 65 | disable: isDev, 66 | additionalPrecacheEntries: [{ url: '/~offline', revision }], 67 | }) 68 | const withNextIntl = createNextIntlPlugin() 69 | 70 | export default withSerwist(withNextIntl(nextConfig)) 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plex-rewind", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "start": "next start", 8 | "build": "next build", 9 | "lint": "next lint", 10 | "lint:types": "tsc --noEmit", 11 | "lint:css": "stylelint --cache \"**/*.css\"", 12 | "lint:all": "pnpm lint && pnpm lint:types && pnpm lint:css && pnpm format:check", 13 | "lint:all:fix": "next lint --fix && pnpm lint:types && stylelint --cache --fix \"**/*.css\" && pnpm format", 14 | "format": "prettier --write --cache .", 15 | "format:check": "prettier --check --cache .", 16 | "prepare": "husky", 17 | "commit": "git-cz" 18 | }, 19 | "config": { 20 | "commitizen": { 21 | "path": "./node_modules/cz-customizable" 22 | } 23 | }, 24 | "dependencies": { 25 | "@heroicons/react": "^2.2.0", 26 | "@internationalized/date": "^3.8.0", 27 | "lodash": "^4.17.21", 28 | "motion": "^12.7.4", 29 | "next": "15.3.1", 30 | "next-auth": "^4.24.11", 31 | "next-intl": "^4.0.2", 32 | "next-runtime-env": "^3.3.0", 33 | "qs": "^6.14.0", 34 | "react": "19.1.0", 35 | "react-aria-components": "^1.8.0", 36 | "react-dom": "19.1.0", 37 | "react-sortablejs": "^6.1.4", 38 | "sharp": "^0.34.1", 39 | "sortablejs": "^1.15.6", 40 | "stories-react": "^1.1.2", 41 | "uuid": "^11.1.0", 42 | "xml2js": "^0.6.2", 43 | "zod": "^3.24.3" 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "^19.8.0", 47 | "@commitlint/config-conventional": "^19.8.0", 48 | "@commitlint/types": "^19.8.0", 49 | "@eslint/eslintrc": "^3.3.1", 50 | "@saithodev/semantic-release-backmerge": "^4.0.1", 51 | "@semantic-release/exec": "^7.0.3", 52 | "@serwist/next": "^9.0.13", 53 | "@stylistic/eslint-plugin": "^4.2.0", 54 | "@tailwindcss/forms": "^0.5.10", 55 | "@tailwindcss/postcss": "^4.1.4", 56 | "@types/lodash": "^4.17.16", 57 | "@types/node": "^22.14.1", 58 | "@types/qs": "^6.9.18", 59 | "@types/react": "19.1.2", 60 | "@types/react-dom": "19.1.2", 61 | "@types/sortablejs": "^1.15.8", 62 | "@types/uuid": "^10.0.0", 63 | "@types/xml2js": "^0.4.14", 64 | "@typescript-eslint/eslint-plugin": "^8.30.1", 65 | "@typescript-eslint/parser": "^8.30.1", 66 | "clsx": "^2.1.1", 67 | "commitizen": "^4.3.1", 68 | "conventional-changelog-conventionalcommits": "^8.0.0", 69 | "cz-customizable": "7.4.0", 70 | "eslint": "^9.24.0", 71 | "eslint-config-next": "15.3.1", 72 | "eslint-config-prettier": "^10.1.2", 73 | "eslint-plugin-jsx-a11y": "^6.10.2", 74 | "eslint-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411", 75 | "eslint-plugin-react-hooks": "^5.2.0", 76 | "husky": "^9.1.7", 77 | "lint-staged": "^15.5.1", 78 | "postcss": "^8.5.3", 79 | "prettier": "^3.5.3", 80 | "prettier-plugin-organize-imports": "^4.1.0", 81 | "prettier-plugin-tailwindcss": "^0.6.11", 82 | "semantic-release": "^24.2.3", 83 | "serwist": "^9.0.13", 84 | "stylelint": "^16.18.0", 85 | "stylelint-config-standard": "^38.0.0", 86 | "stylelint-config-tailwindcss": "^1.0.0", 87 | "tailwindcss": "^4.1.4", 88 | "typescript": "5.8.3" 89 | }, 90 | "pnpm": { 91 | "onlyBuiltDependencies": [ 92 | "sharp" 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | export default { 3 | singleQuote: true, 4 | jsxSingleQuote: true, 5 | semi: false, 6 | tabWidth: 2, 7 | useTabs: false, 8 | overrides: [ 9 | { 10 | files: '*.svg', 11 | options: { 12 | parser: 'html', 13 | }, 14 | }, 15 | ], 16 | plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'], 17 | } 18 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #262626 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaunoT/plex-rewind/6688f60565d2e2ee4f30438453b9950b07790f87/public/clouds.png -------------------------------------------------------------------------------- /public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaunoT/plex-rewind/6688f60565d2e2ee4f30438453b9950b07790f87/public/icon-192x192.png -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaunoT/plex-rewind/6688f60565d2e2ee4f30438453b9950b07790f87/public/icon-512x512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Plex Rewind", 3 | "short_name": "Plex Rewind", 4 | "icons": [ 5 | { 6 | "src": "/icon-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/icon-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#262626", 17 | "background_color": "#262626", 18 | "start_url": "/", 19 | "display": "standalone", 20 | "orientation": "portrait" 21 | } 22 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaunoT/plex-rewind/6688f60565d2e2ee4f30438453b9950b07790f87/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/twinkling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaunoT/plex-rewind/6688f60565d2e2ee4f30438453b9950b07790f87/public/twinkling.png -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: [ 3 | [ 4 | '@semantic-release/commit-analyzer', 5 | { 6 | preset: 'conventionalcommits', 7 | releaseRules: [ 8 | { breaking: true, release: 'major' }, 9 | { type: 'feat', release: 'minor' }, 10 | { type: 'fix', release: 'patch' }, 11 | { type: 'ui', release: 'patch' }, 12 | { type: 'perf', release: 'patch' }, 13 | { type: 'build', release: 'patch' }, 14 | { type: 'ci', release: 'patch' }, 15 | { type: 'revert', release: 'patch' }, 16 | { type: 'refactor', release: 'patch' }, 17 | { type: 'docs', release: false }, 18 | { type: 'chore', release: false }, 19 | { type: 'style', release: false }, 20 | { type: 'test', release: false }, 21 | ], 22 | }, 23 | ], 24 | [ 25 | '@semantic-release/release-notes-generator', 26 | { 27 | preset: 'conventionalcommits', 28 | presetConfig: { 29 | types: [ 30 | { 31 | type: 'breaking', 32 | section: '❗ Breaking Changes', 33 | hidden: false, 34 | }, 35 | { type: 'feat', section: '🚀 Features', hidden: false }, 36 | { type: 'fix', section: '🐛 Bug fixes', hidden: false }, 37 | { type: 'ui', section: '🎨 UI changes', hidden: false }, 38 | { 39 | type: 'perf', 40 | section: '⚡ Performance improvements', 41 | hidden: false, 42 | }, 43 | { type: 'build', section: '🚧 Build', hidden: false }, 44 | { type: 'ci', section: '⚙️ CI', hidden: false }, 45 | { type: 'revert', section: '⏪ Reverts', hidden: false }, 46 | { 47 | type: 'refactor', 48 | section: '🔧 Refactor', 49 | hidden: false, 50 | }, 51 | { type: 'docs', section: '📝 Documentation', hidden: false }, 52 | { type: 'test', hidden: true }, 53 | { type: 'chore', hidden: true }, 54 | { type: 'style', hidden: true }, 55 | ], 56 | }, 57 | }, 58 | ], 59 | '@semantic-release/github', 60 | [ 61 | '@semantic-release/exec', 62 | { 63 | verifyReleaseCmd: 64 | 'echo NEXT_VERSION_TAG=${nextRelease.version} >> $GITHUB_ENV', 65 | }, 66 | ], 67 | [ 68 | '@saithodev/semantic-release-backmerge', 69 | { 70 | backmergeBranches: [{ from: 'main', to: 'develop' }], 71 | message: 'chore(release): backmerge [skip ci]', 72 | }, 73 | ], 74 | ], 75 | branches: [ 76 | 'main', 77 | { 78 | name: 'develop', 79 | prerelease: 'develop', 80 | }, 81 | ], 82 | } 83 | -------------------------------------------------------------------------------- /src/app/_assets/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaunoT/plex-rewind/6688f60565d2e2ee4f30438453b9950b07790f87/src/app/_assets/stars.png -------------------------------------------------------------------------------- /src/app/_components/AppProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Version } from '@/types' 4 | import { Settings } from '@/types/settings' 5 | import { checkRequiredSettings } from '@/utils/helpers' 6 | import { 7 | ArrowPathIcon, 8 | CogIcon, 9 | XCircleIcon, 10 | } from '@heroicons/react/24/outline' 11 | import clsx from 'clsx' 12 | import { useSession } from 'next-auth/react' 13 | import { useTranslations } from 'next-intl' 14 | import Image from 'next/image' 15 | import Link from 'next/link' 16 | import { usePathname } from 'next/navigation' 17 | import { ReactNode, useEffect, useState } from 'react' 18 | import stars from '../_assets/stars.png' 19 | import GlobalContextProvider from './GlobalContextProvider' 20 | import LocaleSelect from './LocaleSelect' 21 | 22 | type Props = { 23 | children: ReactNode 24 | settings: Settings 25 | version: Version 26 | } 27 | 28 | export default function AppProvider({ children, settings, version }: Props) { 29 | const pathname = usePathname() 30 | const missingSetting = checkRequiredSettings(settings) 31 | const { data: session } = useSession() 32 | const [isSettings, setIsSettings] = useState( 33 | pathname.startsWith('/settings'), 34 | ) 35 | const [settingsLink, setSettingsLink] = useState('/settings/general') 36 | const t = useTranslations('AppProvider') 37 | 38 | useEffect(() => { 39 | switch (true) { 40 | case pathname.startsWith('/dashboard'): 41 | setSettingsLink('/settings/dashboard') 42 | break 43 | case pathname.startsWith('/rewind'): 44 | setSettingsLink('/settings/rewind') 45 | break 46 | default: 47 | setSettingsLink('/settings/general') 48 | break 49 | } 50 | 51 | setIsSettings(pathname.startsWith('/settings')) 52 | }, [pathname]) 53 | 54 | return ( 55 | 56 |
62 |
63 |
64 | {t('starsAlt')} 71 |
72 |
73 |
74 |
75 | 76 |
77 | {version.hasUpdate && session?.user.isAdmin && ( 78 | 84 | 85 | 86 | )} 87 | {!missingSetting && session?.user.isAdmin && ( 88 | 93 | {isSettings ? ( 94 | 95 | ) : ( 96 | 97 | )} 98 | 99 | )} 100 | 101 |
102 | 103 | {children} 104 |
105 |
106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /src/app/_components/CardWrapper.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { ReactNode } from 'react' 3 | 4 | type Props = { 5 | children: ReactNode 6 | className?: string 7 | } 8 | 9 | export default function CardWrapper({ children, className }: Props) { 10 | return ( 11 |
12 | {children} 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/app/_components/GlobalContextProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { DashboardSearchParams } from '@/types/dashboard' 4 | import { createContext, ReactNode, useEffect, useState } from 'react' 5 | 6 | type GlobalContextProps = { 7 | dashboard: { 8 | isPersonal: DashboardSearchParams['personal'] 9 | setIsPersonal: (isPersonal: DashboardSearchParams['personal']) => void 10 | period: DashboardSearchParams['period'] 11 | setPeriod: (period: DashboardSearchParams['period']) => void 12 | sortBy: DashboardSearchParams['sortBy'] 13 | setSortBy: (sortBy: DashboardSearchParams['sortBy']) => void 14 | } 15 | } 16 | 17 | export const GlobalContext = createContext({ 18 | dashboard: { 19 | isPersonal: undefined, 20 | setIsPersonal: () => {}, 21 | period: 'custom', 22 | setPeriod: () => {}, 23 | sortBy: undefined, 24 | setSortBy: () => {}, 25 | }, 26 | }) 27 | 28 | type Props = { 29 | children: ReactNode 30 | } 31 | 32 | export default function GlobalContextProvider({ children }: Props) { 33 | const [isPersonal, setIsPersonal] = useState< 34 | DashboardSearchParams['personal'] 35 | >(() => { 36 | if (typeof window !== 'undefined') { 37 | return localStorage.getItem( 38 | 'dashboardPersonal', 39 | ) as DashboardSearchParams['personal'] 40 | } 41 | 42 | return undefined 43 | }) 44 | const [period, setPeriod] = useState(() => { 45 | if (typeof window !== 'undefined') { 46 | return localStorage.getItem( 47 | 'dashboardPeriod', 48 | ) as DashboardSearchParams['period'] 49 | } 50 | 51 | return undefined 52 | }) 53 | const [sortBy, setSortBy] = useState(() => { 54 | if (typeof window !== 'undefined') { 55 | return localStorage.getItem( 56 | 'dashboardSort', 57 | ) as DashboardSearchParams['sortBy'] 58 | } 59 | 60 | return undefined 61 | }) 62 | 63 | useEffect(() => { 64 | localStorage.setItem('dashboardPersonal', isPersonal || '') 65 | }, [isPersonal]) 66 | 67 | useEffect(() => { 68 | localStorage.setItem('dashboardPeriod', period || '') 69 | }, [period]) 70 | 71 | useEffect(() => { 72 | localStorage.setItem('dashboardSort', sortBy || '') 73 | }, [sortBy]) 74 | 75 | return ( 76 | 88 | {children} 89 | 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /src/app/_components/GoogleAnalytics.tsx: -------------------------------------------------------------------------------- 1 | import Script from 'next/script' 2 | 3 | export default function GoogleAnalytics({ id }: { id: string }) { 4 | return ( 5 | <> 6 |