diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f84ef54c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,60 @@ +### STANDARD GIT IGNORE FILE ### + +# DEPENDENCIES +node_modules/ +/.pnp +.pnp.js +package-lock.json +yarn.lock + +# TESTING +/coverage +*.lcov +.nyc_output + +# BUILD +build/ +public/build/ +dist/ +generated/ + +# ENV FILES +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# LOGS +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# MISC +.idea +.turbo/ +.cache/ +.next/ +.nuxt/ +tmp/ +temp/ +.eslintcache +.docusaurus + +# MAC +._* +.DS_Store +Thumbs.db + +.turbo +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +Dockerfile +.dockerignore +.git diff --git a/.env.example b/.env.example index 5a98268f..33aff2d9 100644 --- a/.env.example +++ b/.env.example @@ -1,25 +1,37 @@ -# Database configuration -# Replace with your database URL. For example: postgres://user:password@localhost:5432/mydatabase -DATABASE_URL="" - -# NextAuth configuration -# Replace with your NextAuth URL. This is the URL where your application is running. For example: http://localhost:3000 -NEXTAUTH_URL="" - -# Generate a secret for NextAuth with `openssl rand -hex 64` and replace here -NEXTAUTH_SECRET="" - -# Google OAuth configuration -# Replace with your Google Client ID and Secret. These are obtained from the Google Developer Console. -GOOGLE_CLIENT_ID="" -GOOGLE_CLIENT_SECRET="" - -# Node environment -# Set to "development" for development mode or "production" for production mode -NODE_ENV="development" - -# Vercel KV configuration -KV_URL="" -KV_REST_API_URL="" -KV_REST_API_TOKEN="" -KV_REST_API_READ_ONLY_TOKEN="" +# DATABASE CONFIGURATION +# The application uses a PostgreSQL database. You can obtain the database URL from your database provider. +DATABASE_URL=postgres://username:password@localhost:5432/your-database + +# NEXTAUTH CONFIGURATION +# NextAuth is used for authentication in the application. You need to specify the URL where the Next.js application is running and a secret for session cookies. + +NEXTAUTH_URL=http://localhost:3000 + +# You can generate a random secret using a tool like `uuidgen` or any other random string generator. +NEXTAUTH_SECRET= + +# GOOGLE AUTHENTICATION CONFIGURATION +# The application supports authentication with Google. You need to create a project in the Google Developers Console, enable the Google Sign-In API, and create OAuth 2.0 credentials. +# More information: https://developers.google.com/identity/sign-in/web/sign-in +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# SUPABASE CONFIGURATION +# The application uses Supabase for real-time updates. You need to create a project in Supabase and obtain the URL and the anonymous key. +# More information: https://supabase.io/docs/guides/with-nextjs + +# Example: https://yourproject.supabase.co +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_ANON_KEY= + +# WEBSOCKET CONFIGURATION +# The application uses a WebSocket server for real-time communication. Specify the URL of the WebSocket server. +# Example: http://localhost:8080 +NEXT_PUBLIC_SOCKET_URL= + +# UPSTASH CONFIGURATION +# The application uses Upstash for rate limiting. +# You need to create an account on Upstash and obtain the Redis REST URL and Token. +# More information: https://upstash.com +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= diff --git a/.env.local.example b/.env.local.example deleted file mode 100644 index d62837ee..00000000 --- a/.env.local.example +++ /dev/null @@ -1,9 +0,0 @@ -# Supabase configuration -# Replace with your Supabase URL. This is obtained from your Supabase project settings. -NEXT_PUBLIC_SUPABASE_URL="" - -# Replace with your Supabase Anon Key. This is obtained from your Supabase project API settings. -NEXT_PUBLIC_SUPABASE_ANON_KEY="" - -# Replace with your Socket URL. -NEXT_PUBLIC_SOCKET_URL="" diff --git a/.eslintrc.js b/.eslintrc.js index 22c5f467..6ed9a77e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,6 @@ module.exports = { "plugin:jsx-a11y/recommended", "plugin:testing-library/react", "plugin:jest-dom/recommended", - "plugin:cypress/recommended", "plugin:@tanstack/eslint-plugin-query/recommended", "plugin:tailwindcss/recommended", "prettier", @@ -23,7 +22,6 @@ module.exports = { "@typescript-eslint", "testing-library", "jest-dom", - "cypress", "@tanstack/query", "prettier", ], diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..21505a98 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,37 @@ +name: Deploy +on: + push: + branches: + - main + - develop + pull_request: {} + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: 👀 Read app name + uses: SebRollen/toml-action@v1.0.2 + id: app_name + with: + file: "fly.toml" + field: "app" + + - name: Setup fly + uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Deploy Staging + if: ${{ github.ref == 'develop' }} + run: flyctl deploy --remote-only --build-arg NEXT_PUBLIC_SOCKET_URL=${{ secrets.NEXT_PUBLIC_SOCKET_URL }} --build-arg NEXT_PUBLIC_SUPABASE_URL=${{secrets.NEXT_PUBLIC_SUPABASE_URL}} --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=${{secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY}} --app ${{ steps.app_name.outputs.value }}-staging + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + + - name: Deploy Production + if: ${{ github.ref == 'refs/heads/main' }} + run: flyctl deploy --remote-only --build-arg NEXT_PUBLIC_SOCKET_URL=${{ secrets.NEXT_PUBLIC_SOCKET_URL }} --build-arg NEXT_PUBLIC_SUPABASE_URL=${{secrets.NEXT_PUBLIC_SUPABASE_URL}} --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=${{secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY}} + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3d82629e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +ARG NODE_VERSION=20.10.0 +# base image +FROM node:${NODE_VERSION}-slim as base + +# Set output property to standalone for minimal image size +ENV BUILD_STANDALONE="true" + +# Disable telemetry +ENV NEXT_TELEMETRY_DISABLED="1" + +# Set environment variables needed for the build process +ARG NEXT_PUBLIC_SUPABASE_URL +ENV NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} + +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY +ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY} + +ARG NEXT_PUBLIC_SOCKET_URL +ENV NEXT_PUBLIC_SOCKET_URL=${NEXT_PUBLIC_SOCKET_URL} + +# Install openssl for Prisma +RUN apt-get update && apt-get install -y openssl + +# Install pnpm +ARG PNPM_VERSION=8.15.1 +RUN npm install -g pnpm@$PNPM_VERSION + +# Install dependencies +FROM base as deps +WORKDIR /app + +# Install node modules +COPY --link package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +# Build the application +FROM base as build +WORKDIR /app + +# Copy application code +COPY src/ src/ +COPY public/ public/ +COPY prisma/schema.prisma prisma/ +COPY next.config.js postcss.config.js tailwind.config.js tsconfig.json reset.d.ts ./ + +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/package.json ./package.json +COPY --from=deps /app/pnpm-lock.yaml ./pnpm-lock.yaml + +# Generate Prisma Client +RUN npx prisma generate + +# Build application +RUN pnpm build + +# Production image +FROM base as prod +WORKDIR /app + +ENV NODE_ENV="production" + +COPY --from=build /app/public ./public +COPY --from=build /app/.next/standalone ./ +COPY --from=build /app/.next/static ./.next/static + +EXPOSE 3000 +CMD [ "node", "server.js" ] diff --git a/cypress.config.ts b/cypress.config.ts deleted file mode 100644 index 085416e2..00000000 --- a/cypress.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from "cypress"; - -export default defineConfig({ - e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here - }, - }, - - component: { - devServer: { - framework: "next", - bundler: "webpack", - }, - }, -}); diff --git a/cypress/e2e/1-getting-started/todo.cy.js b/cypress/e2e/1-getting-started/todo.cy.js deleted file mode 100644 index 5bb1a687..00000000 --- a/cypress/e2e/1-getting-started/todo.cy.js +++ /dev/null @@ -1,142 +0,0 @@ -/// -// Welcome to Cypress! -// -// This spec file contains a variety of sample tests -// for a todo list app that are designed to demonstrate -// the power of writing tests in Cypress. -// -// To learn more about how Cypress works and -// what makes it such an awesome testing tool, -// please read our getting started guide: -// https://on.cypress.io/introduction-to-cypress - -describe("example to-do app", () => { - beforeEach(() => { - // Cypress starts out with a blank slate for each test - // so we must tell it to visit our website with the `cy.visit()` command. - // Since we want to visit the same URL at the start of all our tests, - // we include it in our beforeEach function so that it runs before each test - cy.visit("https://example.cypress.io/todo"); - }); - - it("displays two todo items by default", () => { - // We use the `cy.get()` command to get all elements that match the selector. - // Then, we use `should` to assert that there are two matched items, - // which are the two default items. - cy.get(".todo-list li").should("have.length", 2); - - // We can go even further and check that the default todos each contain - // the correct text. We use the `first` and `last` functions - // to get just the first and last matched elements individually, - // and then perform an assertion with `should`. - cy.get(".todo-list li").first().should("have.text", "Pay electric bill"); - cy.get(".todo-list li").last().should("have.text", "Walk the dog"); - }); - - it("can add new todo items", () => { - // We'll store our item text in a variable so we can reuse it - const newItem = "Feed the cat"; - - // Let's get the input element and use the `type` command to - // input our new list item. After typing the content of our item, - // we need to type the enter key as well in order to submit the input. - // This input has a data-test attribute so we'll use that to select the - // element in accordance with best practices: - // https://on.cypress.io/selecting-elements - cy.get("[data-test=new-todo]").type(`${newItem}{enter}`); - - // Now that we've typed our new item, let's check that it actually was added to the list. - // Since it's the newest item, it should exist as the last element in the list. - // In addition, with the two default items, we should have a total of 3 elements in the list. - // Since assertions yield the element that was asserted on, - // we can chain both of these assertions together into a single statement. - cy.get(".todo-list li") - .should("have.length", 3) - .last() - .should("have.text", newItem); - }); - - it("can check off an item as completed", () => { - // In addition to using the `get` command to get an element by selector, - // we can also use the `contains` command to get an element by its contents. - // However, this will yield the