├── .env.example ├── .eslintrc.json ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── incorrect-app-icon.md ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── prettier.config.cjs ├── prisma ├── migrations │ ├── 20230126170225_init │ │ └── migration.sql │ ├── 20230126181934_user_email_fields │ │ └── migration.sql │ ├── 20230126215401_remove_user_stuff │ │ └── migration.sql │ ├── 20230126233521_update_user_id_to_username │ │ └── migration.sql │ ├── 20230127051217_add_user_description │ │ └── migration.sql │ ├── 20230129204228_add_index_on_dock_featured │ │ └── migration.sql │ ├── 20230130042222_add_user_url │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── dockhunt-icon.png ├── favicon.png ├── og-wallpaper-monterey.jpg ├── og-wallpaper.jpg └── opengraph.png ├── src ├── components │ ├── AddDockCard.tsx │ ├── BouncingLoader.tsx │ ├── Dock.tsx │ ├── DockCard.tsx │ └── MenuBar.tsx ├── env │ ├── client.mjs │ ├── schema.mjs │ └── server.mjs ├── images │ ├── basedash.svg │ ├── dockhunt.svg │ ├── github.svg │ ├── npm.svg │ ├── pinned.jpg │ └── twitter.svg ├── pages │ ├── _app.tsx │ ├── add-dock.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ ├── cli │ │ │ ├── check-apps.ts │ │ │ └── icon-upload.ts │ │ ├── og.tsx │ │ └── trpc │ │ │ └── [trpc].ts │ ├── apps.tsx │ ├── apps │ │ └── [appName].tsx │ ├── index.tsx │ ├── new-dock.tsx │ └── users │ │ └── [username].tsx ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ ├── apps.ts │ │ │ ├── docks.ts │ │ │ └── users.ts │ │ └── trpc.ts │ ├── auth.ts │ └── db.ts ├── styles │ └── globals.css ├── types │ └── next-auth.d.ts └── utils │ ├── api.ts │ └── constants.ts ├── tailwind.config.cjs └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo. 2 | # Keep this file up-to-date when you add new variables to `.env`. 3 | 4 | # This file will be committed to version control, so make sure not to have any secrets in it. 5 | # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. 6 | 7 | # When adding additional env variables, the schema in /env/schema.mjs should be updated accordingly 8 | # Prisma 9 | DATABASE_URL= 10 | 11 | # Next Auth 12 | # You can generate the secret via 'openssl rand -base64 32' on Linux 13 | # More info: https://next-auth.js.org/configuration/options#secret 14 | # NEXTAUTH_SECRET= 15 | NEXTAUTH_URL= 16 | 17 | # Next Auth Twitter Provider 18 | TWITTER_CLIENT_ID= 19 | TWITTER_CLIENT_SECRET= 20 | 21 | BUCKET_ENDPOINT= 22 | S3_ACCESS_KEY_ID= 23 | S3_SECRET_ACCESS_KEY= 24 | 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "extends": [ 5 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 6 | ], 7 | "files": ["*.ts", "*.tsx"], 8 | "parserOptions": { 9 | "project": "tsconfig.json" 10 | } 11 | } 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "project": "./tsconfig.json" 16 | }, 17 | "plugins": ["@typescript-eslint"], 18 | "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 19 | "rules": { 20 | "@typescript-eslint/consistent-type-imports": "warn" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.basedash.com'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 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 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 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 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/incorrect-app-icon.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Incorrect app icon 3 | about: An app isn't using the correct icon on the website 4 | title: Incorrect app icon for 5 | labels: incorrect app icon 6 | assignees: '' 7 | 8 | --- 9 | 10 | App name: 11 | Link to app on Dockhunt: 12 | 13 | I've included the correct `.icns` file below, found in my Applications directory by right-clicking the app, selecting "Show Package Contents", and navigating to Contents > Resources. 14 | -------------------------------------------------------------------------------- /.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 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | 44 | .idea 45 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dockhunt 2 | 3 | [![Dockhunt - Discover the apps everyone is docking about](https://user-images.githubusercontent.com/15393239/215352336-3a2e63e2-b474-45a9-9721-160cecb83325.png)](https://www.dockhunt.com) 4 | 5 | [Website](https://www.dockhunt.com) ⋅ [Twitter](https://twitter.com/dockhuntapp) ⋅ [npm](https://www.npmjs.com/package/dockhunt) 6 | 7 | [CLI tool code](https://github.com/Basedash/dockhunt-cli) 8 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 4 | * This is especially useful for Docker builds. 5 | */ 6 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs")); 7 | 8 | /** @type {import("next").NextConfig} */ 9 | const config = { 10 | reactStrictMode: true, 11 | /* If trying out the experimental appDir, comment the i18n config out 12 | * @see https://github.com/vercel/next.js/issues/41980 */ 13 | i18n: { 14 | locales: ["en"], 15 | defaultLocale: "en", 16 | }, 17 | images: { 18 | remotePatterns: [ 19 | // Twitter profile images 20 | { 21 | protocol: "https", 22 | hostname: "pbs.twimg.com", 23 | pathname: "/profile_images/**", 24 | }, 25 | // Twitter default profile image 26 | { 27 | protocol: "https", 28 | hostname: "abs.twimg.com", 29 | pathname: "/sticky/**", 30 | }, 31 | // App icons in DigitalOcean bucket 32 | { 33 | protocol: "https", 34 | hostname: "dockhunt-images.nyc3.cdn.digitaloceanspaces.com", 35 | pathname: "/*", 36 | }, 37 | ], 38 | }, 39 | eslint: { 40 | ignoreDuringBuilds: true, 41 | }, 42 | }; 43 | export default config; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dockhunt", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "postinstall": "prisma generate", 9 | "generate": "prisma generate", 10 | "lint": "next lint", 11 | "start": "next start", 12 | "migrate": "prisma migrate dev" 13 | }, 14 | "dependencies": { 15 | "@aws-sdk/abort-controller": "^3.257.0", 16 | "@next-auth/prisma-adapter": "^1.0.5", 17 | "@prisma/client": "4.9.0", 18 | "@radix-ui/react-scroll-area": "^1.0.2", 19 | "@radix-ui/react-tooltip": "^1.0.3", 20 | "@tanstack/react-query": "^4.20.0", 21 | "@trpc/client": "^10.8.1", 22 | "@trpc/next": "^10.8.1", 23 | "@trpc/react-query": "^10.8.1", 24 | "@trpc/server": "^10.8.1", 25 | "@vercel/og": "^0.0.27", 26 | "aws-sdk": "^2.1303.0", 27 | "date-fns": "^2.29.3", 28 | "formidable": "^2.1.1", 29 | "framer-motion": "^8.5.3", 30 | "next": "13.1.2", 31 | "next-auth": "^4.23.1", 32 | "next-connect": "^0.13.0", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "superjson": "1.9.1", 36 | "uuid": "^9.0.0", 37 | "zod": "^3.20.2" 38 | }, 39 | "devDependencies": { 40 | "@types/formidable": "^2.0.5", 41 | "@types/multer": "^1.4.7", 42 | "@types/multer-s3": "^3.0.0", 43 | "@types/node": "^18.11.18", 44 | "@types/prettier": "^2.7.2", 45 | "@types/react": "^18.0.26", 46 | "@types/react-dom": "^18.0.10", 47 | "@types/uuid": "^9.0.0", 48 | "@typescript-eslint/eslint-plugin": "^5.47.1", 49 | "@typescript-eslint/parser": "^5.47.1", 50 | "autoprefixer": "^10.4.7", 51 | "eslint": "^8.30.0", 52 | "eslint-config-next": "13.1.2", 53 | "postcss": "^8.4.14", 54 | "prettier": "^2.8.1", 55 | "prettier-plugin-tailwindcss": "^0.2.1", 56 | "prisma": "4.9.0", 57 | "tailwindcss": "^3.2.0", 58 | "typescript": "^4.9.4" 59 | }, 60 | "ct3aMetadata": { 61 | "initVersion": "7.3.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | module.exports = { 3 | plugins: [require.resolve("prettier-plugin-tailwindcss")], 4 | }; 5 | -------------------------------------------------------------------------------- /prisma/migrations/20230126170225_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "App" ( 3 | "name" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "iconUrl" TEXT, 7 | "description" TEXT, 8 | "websiteUrl" TEXT, 9 | "twitterUrl" TEXT, 10 | 11 | CONSTRAINT "App_pkey" PRIMARY KEY ("name") 12 | ); 13 | 14 | -- CreateTable 15 | CREATE TABLE "Dock" ( 16 | "id" TEXT NOT NULL, 17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "updatedAt" TIMESTAMP(3) NOT NULL, 19 | "featured" BOOLEAN NOT NULL DEFAULT false, 20 | "userId" TEXT NOT NULL, 21 | 22 | CONSTRAINT "Dock_pkey" PRIMARY KEY ("id") 23 | ); 24 | 25 | -- CreateTable 26 | CREATE TABLE "DockItem" ( 27 | "id" TEXT NOT NULL, 28 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | "updatedAt" TIMESTAMP(3) NOT NULL, 30 | "appId" TEXT NOT NULL, 31 | "position" INTEGER NOT NULL, 32 | "dockId" TEXT NOT NULL, 33 | 34 | CONSTRAINT "DockItem_pkey" PRIMARY KEY ("id") 35 | ); 36 | 37 | -- CreateTable 38 | CREATE TABLE "Account" ( 39 | "id" TEXT NOT NULL, 40 | "userId" TEXT NOT NULL, 41 | "type" TEXT NOT NULL, 42 | "provider" TEXT NOT NULL, 43 | "providerAccountId" TEXT NOT NULL, 44 | "refresh_token" TEXT, 45 | "access_token" TEXT, 46 | "expires_at" INTEGER, 47 | "token_type" TEXT, 48 | "scope" TEXT, 49 | "id_token" TEXT, 50 | "session_state" TEXT, 51 | 52 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 53 | ); 54 | 55 | -- CreateTable 56 | CREATE TABLE "Session" ( 57 | "id" TEXT NOT NULL, 58 | "sessionToken" TEXT NOT NULL, 59 | "userId" TEXT NOT NULL, 60 | "expires" TIMESTAMP(3) NOT NULL, 61 | 62 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 63 | ); 64 | 65 | -- CreateTable 66 | CREATE TABLE "User" ( 67 | "id" TEXT NOT NULL, 68 | "name" TEXT, 69 | "image" TEXT, 70 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 71 | "updatedAt" TIMESTAMP(3) NOT NULL, 72 | "avatarUrl" TEXT, 73 | "twitterHandle" TEXT, 74 | "twitterFollowerCount" INTEGER, 75 | 76 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 77 | ); 78 | 79 | -- CreateTable 80 | CREATE TABLE "VerificationToken" ( 81 | "identifier" TEXT NOT NULL, 82 | "token" TEXT NOT NULL, 83 | "expires" TIMESTAMP(3) NOT NULL 84 | ); 85 | 86 | -- CreateIndex 87 | CREATE UNIQUE INDEX "Dock_userId_key" ON "Dock"("userId"); 88 | 89 | -- CreateIndex 90 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 91 | 92 | -- CreateIndex 93 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 94 | 95 | -- CreateIndex 96 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 97 | 98 | -- CreateIndex 99 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 100 | 101 | -- AddForeignKey 102 | ALTER TABLE "Dock" ADD CONSTRAINT "Dock_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 103 | 104 | -- AddForeignKey 105 | ALTER TABLE "DockItem" ADD CONSTRAINT "DockItem_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App"("name") ON DELETE CASCADE ON UPDATE CASCADE; 106 | 107 | -- AddForeignKey 108 | ALTER TABLE "DockItem" ADD CONSTRAINT "DockItem_dockId_fkey" FOREIGN KEY ("dockId") REFERENCES "Dock"("id") ON DELETE CASCADE ON UPDATE CASCADE; 109 | 110 | -- AddForeignKey 111 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 112 | 113 | -- AddForeignKey 114 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 115 | -------------------------------------------------------------------------------- /prisma/migrations/20230126181934_user_email_fields/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" ADD COLUMN "email" TEXT, 9 | ADD COLUMN "emailVerified" TIMESTAMP(3); 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 13 | -------------------------------------------------------------------------------- /prisma/migrations/20230126215401_remove_user_stuff/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `image` on the `User` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" DROP COLUMN "image"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20230126233521_update_user_id_to_username/migration.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | -- Default values for updatedAt 4 | ALTER TABLE "App" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; 5 | ALTER TABLE "Dock" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; 6 | ALTER TABLE "DockItem" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; 7 | ALTER TABLE "User" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; 8 | 9 | -- Update User columns 10 | ALTER TABLE "User" RENAME COLUMN "twitterHandle" TO "username"; 11 | ALTER TABLE "User" ALTER COLUMN "name" SET NOT NULL; 12 | 13 | COMMIT; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20230127051217_add_user_description/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail. 5 | - Made the column `username` on table `User` required. This step will fail if there are existing NULL values in that column. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "User" ADD COLUMN "description" TEXT, 10 | ALTER COLUMN "username" SET NOT NULL; 11 | 12 | -- CreateIndex 13 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 14 | -------------------------------------------------------------------------------- /prisma/migrations/20230129204228_add_index_on_dock_featured/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "Dock_featured_idx" ON "Dock"("featured"); 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230130042222_add_user_url/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "url" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model App { 11 | name String @id 12 | createdAt DateTime @default(now()) 13 | updatedAt DateTime @default(now()) @updatedAt 14 | iconUrl String? // png of the app icon 15 | description String? 16 | websiteUrl String? 17 | twitterUrl String? 18 | dockItems DockItem[] 19 | } 20 | 21 | model Dock { 22 | id String @id @default(cuid()) 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @default(now()) @updatedAt 25 | featured Boolean @default(false) 26 | userId String @unique 27 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 28 | dockItems DockItem[] 29 | 30 | @@index([featured]) 31 | } 32 | 33 | model DockItem { 34 | id String @id @default(cuid()) 35 | createdAt DateTime @default(now()) 36 | updatedAt DateTime @default(now()) @updatedAt 37 | appId String 38 | app App @relation(fields: [appId], references: [name], onDelete: Cascade) 39 | position Int 40 | dockId String 41 | dock Dock @relation(fields: [dockId], references: [id], onDelete: Cascade) 42 | } 43 | 44 | // Necessary for Next auth 45 | model Account { 46 | id String @id @default(cuid()) 47 | userId String 48 | type String 49 | provider String 50 | providerAccountId String 51 | refresh_token String? @db.Text 52 | access_token String? @db.Text 53 | expires_at Int? 54 | token_type String? 55 | scope String? 56 | id_token String? @db.Text 57 | session_state String? 58 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 59 | 60 | @@unique([provider, providerAccountId]) 61 | } 62 | 63 | model Session { 64 | id String @id @default(cuid()) 65 | sessionToken String @unique 66 | userId String 67 | expires DateTime 68 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 69 | } 70 | 71 | model User { 72 | id String @id @default(cuid()) 73 | username String @unique // From Twitter 74 | name String // From Twitter 75 | description String? // From Twitter 76 | url String? // From Twitter 77 | accounts Account[] 78 | sessions Session[] 79 | email String? @unique 80 | emailVerified DateTime? 81 | createdAt DateTime @default(now()) 82 | updatedAt DateTime @default(now()) @updatedAt 83 | avatarUrl String? // From Twitter 84 | twitterFollowerCount Int? // From Twitter 85 | dock Dock? 86 | } 87 | 88 | model VerificationToken { 89 | identifier String 90 | token String @unique 91 | expires DateTime 92 | 93 | @@unique([identifier, token]) 94 | } 95 | -------------------------------------------------------------------------------- /public/dockhunt-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basedash/dockhunt/ec885c65225f7295e0e9813d3169dceb96632ab6/public/dockhunt-icon.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basedash/dockhunt/ec885c65225f7295e0e9813d3169dceb96632ab6/public/favicon.png -------------------------------------------------------------------------------- /public/og-wallpaper-monterey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basedash/dockhunt/ec885c65225f7295e0e9813d3169dceb96632ab6/public/og-wallpaper-monterey.jpg -------------------------------------------------------------------------------- /public/og-wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basedash/dockhunt/ec885c65225f7295e0e9813d3169dceb96632ab6/public/og-wallpaper.jpg -------------------------------------------------------------------------------- /public/opengraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basedash/dockhunt/ec885c65225f7295e0e9813d3169dceb96632ab6/public/opengraph.png -------------------------------------------------------------------------------- /src/components/AddDockCard.tsx: -------------------------------------------------------------------------------- 1 | import { useSession } from "next-auth/react"; 2 | import Link from "next/link"; 3 | import { api } from "utils/api"; 4 | import { desktopAppDownloadLink } from "utils/constants"; 5 | 6 | export function AddDockCard() { 7 | const { data: sessionData } = useSession(); 8 | const user = api.users.getOne.useQuery({ id: sessionData?.user?.id ?? "" }); 9 | 10 | if (user.data?.dock) { 11 | return null; 12 | } 13 | 14 | return ( 15 |
16 |
17 |
18 |

19 | Want to add your own dock? Run this command in your terminal: 20 |

21 |

22 | Want to add your own dock? Run this command on a macOS device: 23 |

24 | 25 | npx dockhunt 26 | 27 |

28 | Or install the desktop app. 29 |

30 | 31 | More details → 32 | 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/BouncingLoader.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { motion } from "framer-motion"; 3 | 4 | const NextImage = motion(Image); 5 | 6 | export const BouncingLoader = () => { 7 | return ( 8 |
9 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Dock.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import type { App } from "@prisma/client"; 3 | import Link from "next/link"; 4 | import * as Tooltip from "@radix-ui/react-tooltip"; 5 | import { placeholderImageUrl } from "utils/constants"; 6 | import Image from "next/image"; 7 | import * as ScrollArea from '@radix-ui/react-scroll-area'; 8 | 9 | const DockImage = motion(Image); 10 | 11 | const DockItem = ({ app }: { app: App }) => { 12 | const variants = { 13 | hover: { 14 | width: 92, 15 | height: 80, 16 | }, 17 | initial: { 18 | width: 80, 19 | height: 80, 20 | }, 21 | }; 22 | 23 | return ( 24 | 25 | 26 | 27 | 39 | 65 | 66 | 67 | 68 | 69 | 70 |
75 | {app.name} 76 |
77 |
78 |
79 |
80 | ); 81 | }; 82 | 83 | export function Dock({ apps }: { apps: App[] }) { 84 | return ( 85 |
86 | {/* Dock background */} 87 |
88 | {/* Scrollable container */} 89 | 90 | 91 |
92 | {apps.map((app) => ( 93 | 94 | ))} 95 |
96 |
97 | {/* TODO: Style the scrollbar: https://www.radix-ui.com/docs/primitives/components/scroll-area */} 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/components/DockCard.tsx: -------------------------------------------------------------------------------- 1 | import * as Tooltip from "@radix-ui/react-tooltip"; 2 | import type { inferRouterOutputs } from "@trpc/server"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import type { AppRouter } from "server/api/root"; 6 | 7 | import { Dock as DockComponent } from "./Dock"; 8 | 9 | export function DockCard({ 10 | dock, 11 | }: { 12 | dock: inferRouterOutputs["docks"]["getFeatured"][0]; 13 | }) { 14 | return ( 15 |
16 |

17 | 18 | {dock.user.name} 19 | 20 | 21 | 27 | @{dock.user.username} 28 | 29 |

30 | 31 |
32 | 33 | 34 | 35 | {/* TODO: Use placeholder image for null values */} 36 | {`${dock.user.name}'s 43 | 44 | 45 | 46 | 47 |
48 |

{dock.user.name}

49 | {dock.user.description && ( 50 |

{dock.user.description}

51 | )} 52 |
53 |
54 |
55 |
56 | 57 | 61 | 62 |
63 | dockItem.app)} 65 | /> 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/MenuBar.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | import basedash from "images/basedash.svg"; 3 | import dockhunt from "images/dockhunt.svg"; 4 | import github from "images/github.svg"; 5 | import npm from "images/npm.svg"; 6 | import twitter from "images/twitter.svg"; 7 | import { signIn, signOut, useSession } from "next-auth/react"; 8 | import Image from "next/image"; 9 | import Link from "next/link"; 10 | import { useEffect, useState } from "react"; 11 | import { api } from "../utils/api"; 12 | 13 | export const MenuBar = () => { 14 | const { data: sessionData } = useSession(); 15 | const user = api.users.getOne.useQuery({ id: sessionData?.user?.id ?? "" }); 16 | 17 | const [date, setDate] = useState(new Date()); 18 | 19 | useEffect(() => { 20 | const timer = setInterval(() => { 21 | setDate(new Date()); 22 | }, 5000); // Update every 5 seconds so we don't get too out of sync 23 | 24 | return () => clearInterval(timer); 25 | }, []); 26 | 27 | return ( 28 |
29 |
30 | 31 | Dockhunt 32 | Dockhunt 33 | 34 | 35 | Top apps 36 | 37 | 38 | {user.data?.dock ? "Update your dock" : "Add your dock"} 39 | 40 | 41 | Made by Basedash 42 | 43 |
44 | 45 |
46 | 47 | Basedash 48 | 49 | 54 | Twitter 55 | 56 | 61 | GitHub 62 | 63 | 69 | npm 70 | 71 |
72 | {format(date, "eee MMM d p")} 73 |
74 | {sessionData && sessionData.user && ( 75 | 76 | {sessionData.user.name} 77 | 78 | )} 79 | 88 |
89 |
90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /src/env/client.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { clientEnv, clientSchema } from "./schema.mjs"; 3 | 4 | const _clientEnv = clientSchema.safeParse(clientEnv); 5 | 6 | export const formatErrors = ( 7 | /** @type {import('zod').ZodFormattedError,string>} */ 8 | errors, 9 | ) => 10 | Object.entries(errors) 11 | .map(([name, value]) => { 12 | if (value && "_errors" in value) 13 | return `${name}: ${value._errors.join(", ")}\n`; 14 | }) 15 | .filter(Boolean); 16 | 17 | if (!_clientEnv.success) { 18 | console.error( 19 | "❌ Invalid environment variables:\n", 20 | ...formatErrors(_clientEnv.error.format()), 21 | ); 22 | throw new Error("Invalid environment variables"); 23 | } 24 | 25 | for (let key of Object.keys(_clientEnv.data)) { 26 | if (!key.startsWith("NEXT_PUBLIC_")) { 27 | console.warn( 28 | `❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`, 29 | ); 30 | 31 | throw new Error("Invalid public environment variable name"); 32 | } 33 | } 34 | 35 | export const env = _clientEnv.data; 36 | -------------------------------------------------------------------------------- /src/env/schema.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { z } from "zod"; 3 | 4 | /** 5 | * Specify your server-side environment variables schema here. 6 | * This way you can ensure the app isn't built with invalid env vars. 7 | */ 8 | export const serverSchema = z.object({ 9 | DATABASE_URL: z.string().url(), 10 | NODE_ENV: z.enum(["development", "test", "production"]), 11 | NEXTAUTH_SECRET: 12 | process.env.NODE_ENV === "production" 13 | ? z.string().min(1) 14 | : z.string().min(1).optional(), 15 | NEXTAUTH_URL: z.preprocess( 16 | // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL 17 | // Since NextAuth.js automatically uses the VERCEL_URL if present. 18 | (str) => process.env.VERCEL_URL ?? str, 19 | // VERCEL_URL doesn't include `https` so it cant be validated as a URL 20 | process.env.VERCEL ? z.string() : z.string().url(), 21 | ), 22 | TWITTER_CLIENT_ID: z.string(), 23 | TWITTER_CLIENT_SECRET: z.string(), 24 | BUCKET_ENDPOINT: z.string().url(), 25 | S3_ACCESS_KEY_ID: z.string(), 26 | S3_SECRET_ACCESS_KEY: z.string(), 27 | }); 28 | 29 | /** 30 | * You can't destruct `process.env` as a regular object in the Next.js 31 | * middleware, so you have to do it manually here. 32 | * @type {{ [k in keyof z.infer]: z.infer[k] | undefined }} 33 | */ 34 | export const serverEnv = { 35 | DATABASE_URL: process.env.DATABASE_URL, 36 | NODE_ENV: process.env.NODE_ENV, 37 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, 38 | NEXTAUTH_URL: process.env.NEXTAUTH_URL, 39 | TWITTER_CLIENT_ID: process.env.TWITTER_CLIENT_ID, 40 | TWITTER_CLIENT_SECRET: process.env.TWITTER_CLIENT_SECRET, 41 | BUCKET_ENDPOINT: process.env.BUCKET_ENDPOINT, 42 | S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID, 43 | S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY, 44 | }; 45 | 46 | /** 47 | * Specify your client-side environment variables schema here. 48 | * This way you can ensure the app isn't built with invalid env vars. 49 | * To expose them to the client, prefix them with `NEXT_PUBLIC_`. 50 | */ 51 | export const clientSchema = z.object({ 52 | NEXT_PUBLIC_URL: z.string(), 53 | }); 54 | 55 | /** 56 | * You can't destruct `process.env` as a regular object, so you have to do 57 | * it manually here. This is because Next.js evaluates this at build time, 58 | * and only used environment variables are included in the build. 59 | * @type {{ [k in keyof z.infer]: z.infer[k] | undefined }} 60 | */ 61 | export const clientEnv = { 62 | NEXT_PUBLIC_URL: process.env.NEXT_PUBLIC_URL, 63 | }; 64 | -------------------------------------------------------------------------------- /src/env/server.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars. 4 | * It has to be a `.mjs`-file to be imported there. 5 | */ 6 | import { serverSchema, serverEnv } from "./schema.mjs"; 7 | import { env as clientEnv, formatErrors } from "./client.mjs"; 8 | 9 | const _serverEnv = serverSchema.safeParse(serverEnv); 10 | 11 | if (!_serverEnv.success) { 12 | console.error( 13 | "❌ Invalid environment variables:\n", 14 | ...formatErrors(_serverEnv.error.format()), 15 | ); 16 | throw new Error("Invalid environment variables"); 17 | } 18 | 19 | for (let key of Object.keys(_serverEnv.data)) { 20 | if (key.startsWith("NEXT_PUBLIC_")) { 21 | console.warn("❌ You are exposing a server-side env-variable:", key); 22 | 23 | throw new Error("You are exposing a server-side env-variable"); 24 | } 25 | } 26 | 27 | export const env = { ..._serverEnv.data, ...clientEnv }; 28 | -------------------------------------------------------------------------------- /src/images/basedash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/dockhunt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/images/npm.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/images/pinned.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basedash/dockhunt/ec885c65225f7295e0e9813d3169dceb96632ab6/src/images/pinned.jpg -------------------------------------------------------------------------------- /src/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { type AppType } from "next/app"; 3 | import { type Session } from "next-auth"; 4 | import * as Tooltip from "@radix-ui/react-tooltip"; 5 | import { SessionProvider } from "next-auth/react"; 6 | import Script from "next/script"; 7 | 8 | import { api } from "../utils/api"; 9 | 10 | import "../styles/globals.css"; 11 | import { MenuBar } from "components/MenuBar"; 12 | 13 | const MyApp: AppType<{ session: Session | null }> = ({ 14 | Component, 15 | pageProps: { session, ...pageProps }, 16 | }) => { 17 | return ( 18 | 19 | 20 |