diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..539d10b --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,62 @@ +{ + "projectName": "next-saas-starter", + "projectOwner": "Blazity", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 64, + "commit": true, + "commitConvention": "none", + "contributors": [ + { + "login": "bmstefanski", + "name": "Bart Stefanski", + "avatar_url": "https://avatars.githubusercontent.com/u/28964599?v=4", + "profile": "https://bstefanski.com/", + "contributions": [ + "code" + ] + }, + { + "login": "ilasota", + "name": "Igor Lasota", + "avatar_url": "https://avatars.githubusercontent.com/u/34578189?v=4", + "profile": "https://github.com/ilasota", + "contributions": [ + "code" + ] + }, + { + "login": "jbryn", + "name": "Jan Bryński", + "avatar_url": "https://avatars.githubusercontent.com/u/52970664?v=4", + "profile": "https://github.com/jbryn", + "contributions": [ + "code" + ] + }, + { + "login": "logan-anderson", + "name": "Logan Anderson", + "avatar_url": "https://avatars.githubusercontent.com/u/43075109?v=4", + "profile": "https://www.logana.dev/", + "contributions": [ + "code", + "doc", + "mentoring" + ] + }, + { + "login": "fdukat", + "name": "Filip Dukat", + "avatar_url": "https://avatars.githubusercontent.com/u/87642690?v=4", + "profile": "https://github.com/fdukat", + "contributions": [ + "doc" + ] + } + ], + "contributorsPerLine": 7 +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bbce85a --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +SENDGRID_API_KEY= +NEXT_PUBLIC_TINA_CLIENT_ID= +NEXT_PUBLIC_EDIT_BRANCH="master" +NEXT_PUBLIC_ORGANIZATION_NAME= +NEXT_PUBLIC_USE_LOCAL_CLIENT="" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..cad3818 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,40 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["react-app", "prettier", "plugin:react/recommended", "next/core-web-vitals"], + "env": { + "es6": true + }, + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "react/prop-types": 0, + "react/react-in-jsx-scope": 0, + "react/display-name": 0, + "no-unused-vars": 0, + "sort-imports": ["error", { "ignoreCase": true, "ignoreDeclarationSort": true }], + "import/order": [ + 1, + { + "groups": ["external", "builtin", "internal", "sibling", "parent", "index"], + "pathGroups": [ + { "pattern": "env", "group": "internal" }, + { "pattern": "types", "group": "internal" }, + { "pattern": "components/**", "group": "internal" }, + { "pattern": "contexts/**", "group": "internal" }, + { "pattern": "hooks/**", "group": "internal" }, + { "pattern": "pages/**", "group": "internal" }, + { "pattern": "views/**", "group": "internal" }, + { "pattern": "utils/**", "group": "internal" }, + { "pattern": "public/**", "group": "internal", "position": "after" }, + { "pattern": "posts/**", "group": "internal", "position": "after" } + ], + "pathGroupsExcludedImportTypes": ["internal"], + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ] + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e1045be --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..8ec3735 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '29 1 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index 8f322f0..46e247a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ yarn-debug.log* yarn-error.log* # local env files +<<<<<<< HEAD .env*.local # vercel @@ -33,3 +34,12 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +======= +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel +>>>>>>> temp-branch diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6bfcf82 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 140, + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/.tina/__generated__/.gitignore b/.tina/__generated__/.gitignore new file mode 100644 index 0000000..5baa59d --- /dev/null +++ b/.tina/__generated__/.gitignore @@ -0,0 +1 @@ +db \ No newline at end of file diff --git a/.tina/__generated__/_graphql.json b/.tina/__generated__/_graphql.json new file mode 100644 index 0000000..f077007 --- /dev/null +++ b/.tina/__generated__/_graphql.json @@ -0,0 +1,1916 @@ +{ + "kind": "Document", + "definitions": [ + { + "kind": "ScalarTypeDefinition", + "name": { + "kind": "Name", + "value": "Reference" + }, + "description": { + "kind": "StringValue", + "value": "References another document, used as a foreign key" + }, + "directives": [] + }, + { + "kind": "ScalarTypeDefinition", + "name": { + "kind": "Name", + "value": "JSON" + }, + "description": { + "kind": "StringValue", + "value": "" + }, + "directives": [] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "SystemInfo" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "filename" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "basename" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "breadcrumbs" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "excludeExtension" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "ListType", + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "path" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "extension" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "template" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Collection" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "PageInfo" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "hasPreviousPage" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "hasNextPage" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "startCursor" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "endCursor" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + } + ] + }, + { + "kind": "InterfaceTypeDefinition", + "description": { + "kind": "StringValue", + "value": "" + }, + "name": { + "kind": "Name", + "value": "Node" + }, + "interfaces": [], + "directives": [], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + } + } + ] + }, + { + "kind": "InterfaceTypeDefinition", + "description": { + "kind": "StringValue", + "value": "" + }, + "name": { + "kind": "Name", + "value": "Document" + }, + "interfaces": [], + "directives": [], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "sys" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "SystemInfo" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "form" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "values" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + } + ] + }, + { + "kind": "InterfaceTypeDefinition", + "description": { + "kind": "StringValue", + "value": "A relay-compliant pagination connection" + }, + "name": { + "kind": "Name", + "value": "Connection" + }, + "interfaces": [], + "directives": [], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "totalCount" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "Query" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getCollection" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Collection" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getCollections" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "ListType", + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Collection" + } + } + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "node" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Node" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getDocumentList" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "before" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "after" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "first" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "last" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentConnection" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getDocumentFields" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getPostsDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getPostsList" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "before" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "after" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "first" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "last" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsConnection" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "DocumentConnectionEdges" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "cursor" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "node" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [ + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Connection" + } + } + ], + "directives": [], + "name": { + "kind": "Name", + "value": "DocumentConnection" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "pageInfo" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PageInfo" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "totalCount" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "edges" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentConnectionEdges" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "Collection" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "name" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "slug" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "label" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "path" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "format" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "matches" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "templates" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "fields" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "documents" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "before" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "after" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "first" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "last" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentConnection" + } + } + } + } + ] + }, + { + "kind": "UnionTypeDefinition", + "name": { + "kind": "Name", + "value": "DocumentNode" + }, + "directives": [], + "types": [ + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "Posts" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "title" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "description" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "date" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "tags" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "imageUrl" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "body" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [ + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Node" + } + }, + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Document" + } + } + ], + "directives": [], + "name": { + "kind": "Name", + "value": "PostsDocument" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "sys" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "SystemInfo" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "data" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Posts" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "form" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "values" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "dataJSON" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "PostsConnectionEdges" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "cursor" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "node" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [ + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Connection" + } + } + ], + "directives": [], + "name": { + "kind": "Name", + "value": "PostsConnection" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "pageInfo" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PageInfo" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "totalCount" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "edges" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsConnectionEdges" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "Mutation" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "addPendingDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "template" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updateDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "params" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentMutation" + } + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "params" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentMutation" + } + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updatePostsDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "params" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsMutation" + } + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createPostsDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "params" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsMutation" + } + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + } + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "DocumentMutation" + }, + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "posts" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsMutation" + } + } + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "PostsMutation" + }, + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "title" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "description" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "date" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "tags" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "imageUrl" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "body" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/.tina/__generated__/_lookup.json b/.tina/__generated__/_lookup.json new file mode 100644 index 0000000..53ab149 --- /dev/null +++ b/.tina/__generated__/_lookup.json @@ -0,0 +1,31 @@ +{ + "DocumentConnection": { + "type": "DocumentConnection", + "resolveType": "multiCollectionDocumentList", + "collections": [ + "posts" + ] + }, + "Node": { + "type": "Node", + "resolveType": "nodeDocument" + }, + "DocumentNode": { + "type": "DocumentNode", + "resolveType": "multiCollectionDocument", + "createDocument": "create", + "updateDocument": "update" + }, + "PostsDocument": { + "type": "PostsDocument", + "resolveType": "collectionDocument", + "collection": "posts", + "createPostsDocument": "create", + "updatePostsDocument": "update" + }, + "PostsConnection": { + "type": "PostsConnection", + "resolveType": "collectionDocumentList", + "collection": "posts" + } +} \ No newline at end of file diff --git a/.tina/__generated__/_schema.json b/.tina/__generated__/_schema.json new file mode 100644 index 0000000..a295513 --- /dev/null +++ b/.tina/__generated__/_schema.json @@ -0,0 +1,276 @@ +{ + "version": { + "fullVersion": "0.59.3", + "major": "0", + "minor": "59", + "patch": "3" + }, + "meta": {}, + "collections": [ + { + "label": "Blog Posts", + "name": "posts", + "path": "posts", + "fields": [ + { + "type": "string", + "label": "Title", + "name": "title", + "namespace": [ + "posts", + "title" + ] + }, + { + "type": "string", + "label": "Description", + "name": "description", + "namespace": [ + "posts", + "description" + ] + }, + { + "type": "string", + "label": "Date", + "name": "date", + "namespace": [ + "posts", + "date" + ] + }, + { + "type": "string", + "label": "Tags", + "name": "tags", + "namespace": [ + "posts", + "tags" + ] + }, + { + "type": "string", + "label": "Image URL", + "name": "imageUrl", + "namespace": [ + "posts", + "imageUrl" + ] + }, + { + "type": "rich-text", + "label": "Blog Post Body", + "name": "body", + "isBody": true, + "templates": [ + { + "name": "Quote", + "label": "Quote", + "fields": [ + { + "type": "string", + "name": "content", + "label": "Content", + "namespace": [ + "posts", + "body", + "Quote", + "content" + ] + }, + { + "type": "string", + "name": "author", + "label": "Author", + "namespace": [ + "posts", + "body", + "Quote", + "author" + ] + }, + { + "type": "string", + "name": "cite", + "label": "Cite", + "namespace": [ + "posts", + "body", + "Quote", + "cite" + ] + } + ], + "namespace": [ + "posts", + "body", + "Quote" + ] + }, + { + "name": "ArticleImage", + "label": "ArticleImage", + "fields": [ + { + "type": "string", + "name": "src", + "label": "Src", + "namespace": [ + "posts", + "body", + "ArticleImage", + "src" + ] + }, + { + "type": "string", + "name": "caption", + "label": "Caption", + "namespace": [ + "posts", + "body", + "ArticleImage", + "caption" + ] + } + ], + "namespace": [ + "posts", + "body", + "ArticleImage" + ] + }, + { + "name": "Code", + "label": "Code", + "fields": [ + { + "type": "string", + "name": "code", + "label": "Code", + "namespace": [ + "posts", + "body", + "Code", + "code" + ] + }, + { + "type": "string", + "name": "language", + "label": "Language", + "namespace": [ + "posts", + "body", + "Code", + "language" + ] + }, + { + "type": "string", + "name": "selectedLines", + "label": "Selected Lines", + "namespace": [ + "posts", + "body", + "Code", + "selectedLines" + ] + }, + { + "type": "boolean", + "name": "withCopyButton", + "label": "With Copy Button", + "namespace": [ + "posts", + "body", + "Code", + "withCopyButton" + ] + }, + { + "type": "boolean", + "name": "withLineNumbers", + "label": "With Line Numbers", + "namespace": [ + "posts", + "body", + "Code", + "withLineNumbers" + ] + }, + { + "type": "string", + "name": "caption", + "label": "Caption", + "namespace": [ + "posts", + "body", + "Code", + "caption" + ] + } + ], + "namespace": [ + "posts", + "body", + "Code" + ] + }, + { + "name": "h2", + "label": "H2", + "inline": true, + "fields": [], + "namespace": [ + "posts", + "body", + "h2" + ] + }, + { + "name": "h3", + "label": "H3", + "inline": true, + "fields": [], + "namespace": [ + "posts", + "body", + "h3" + ] + }, + { + "name": "br", + "label": "BR", + "inline": true, + "fields": [], + "namespace": [ + "posts", + "body", + "br" + ] + }, + { + "name": "p", + "label": "P", + "inline": true, + "fields": [], + "namespace": [ + "posts", + "body", + "p" + ] + } + ], + "namespace": [ + "posts", + "body" + ] + } + ], + "namespace": [ + "posts" + ] + } + ] +} \ No newline at end of file diff --git a/.tina/__generated__/frags.gql b/.tina/__generated__/frags.gql new file mode 100644 index 0000000..595ff50 --- /dev/null +++ b/.tina/__generated__/frags.gql @@ -0,0 +1,8 @@ +fragment PostsParts on Posts { + title + description + date + tags + imageUrl + body +} diff --git a/.tina/__generated__/queries.gql b/.tina/__generated__/queries.gql new file mode 100644 index 0000000..949e404 --- /dev/null +++ b/.tina/__generated__/queries.gql @@ -0,0 +1,38 @@ +query getPostsDocument($relativePath: String!) { + getPostsDocument(relativePath: $relativePath) { + sys { + filename + basename + breadcrumbs + path + relativePath + extension + } + id + data { + ...PostsParts + } + } +} + +query getPostsList { + getPostsList { + totalCount + edges { + node { + id + sys { + filename + basename + breadcrumbs + path + relativePath + extension + } + data { + ...PostsParts + } + } + } + } +} diff --git a/.tina/__generated__/schema.gql b/.tina/__generated__/schema.gql new file mode 100644 index 0000000..c7b1d63 --- /dev/null +++ b/.tina/__generated__/schema.gql @@ -0,0 +1,134 @@ +# DO NOT MODIFY THIS FILE. This file is automatically generated by Tina +"""References another document, used as a foreign key""" +scalar Reference + +"""""" +scalar JSON + +type SystemInfo { + filename: String! + basename: String! + breadcrumbs(excludeExtension: Boolean): [String!]! + path: String! + relativePath: String! + extension: String! + template: String! + collection: Collection! +} + +type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String! + endCursor: String! +} + +"""""" +interface Node { + id: ID! +} + +"""""" +interface Document { + sys: SystemInfo + id: ID! + form: JSON! + values: JSON! +} + +"""A relay-compliant pagination connection""" +interface Connection { + totalCount: Int! +} + +type Query { + getCollection(collection: String): Collection! + getCollections: [Collection!]! + node(id: String): Node! + getDocument(collection: String, relativePath: String): DocumentNode! + getDocumentList(before: String, after: String, first: Int, last: Int): DocumentConnection! + getDocumentFields: JSON! + getPostsDocument(relativePath: String): PostsDocument! + getPostsList(before: String, after: String, first: Int, last: Int): PostsConnection! +} + +type DocumentConnectionEdges { + cursor: String + node: DocumentNode +} + +type DocumentConnection implements Connection { + pageInfo: PageInfo + totalCount: Int! + edges: [DocumentConnectionEdges] +} + +type Collection { + name: String! + slug: String! + label: String + path: String! + format: String + matches: String + templates: [JSON] + fields: [JSON] + documents(before: String, after: String, first: Int, last: Int): DocumentConnection! +} + +union DocumentNode = PostsDocument + +type Posts { + title: String + description: String + date: String + tags: String + imageUrl: String + body: JSON +} + +type PostsDocument implements Node & Document { + id: ID! + sys: SystemInfo! + data: Posts! + form: JSON! + values: JSON! + dataJSON: JSON! +} + +type PostsConnectionEdges { + cursor: String + node: PostsDocument +} + +type PostsConnection implements Connection { + pageInfo: PageInfo + totalCount: Int! + edges: [PostsConnectionEdges] +} + +type Mutation { + addPendingDocument(collection: String!, relativePath: String!, template: String): DocumentNode! + updateDocument(collection: String, relativePath: String!, params: DocumentMutation!): DocumentNode! + createDocument(collection: String, relativePath: String!, params: DocumentMutation!): DocumentNode! + updatePostsDocument(relativePath: String!, params: PostsMutation!): PostsDocument! + createPostsDocument(relativePath: String!, params: PostsMutation!): PostsDocument! +} + +input DocumentMutation { + posts: PostsMutation +} + +input PostsMutation { + title: String + description: String + date: String + tags: String + imageUrl: String + body: JSON +} + +schema { + query: Query + mutation: Mutation +} + \ No newline at end of file diff --git a/.tina/__generated__/types.ts b/.tina/__generated__/types.ts new file mode 100644 index 0000000..c2ff7e8 --- /dev/null +++ b/.tina/__generated__/types.ts @@ -0,0 +1,339 @@ +//@ts-nocheck +// DO NOT MODIFY THIS FILE. This file is automatically generated by Tina +import { gql } from 'tinacms'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + /** References another document, used as a foreign key */ + Reference: any; + JSON: any; +}; + +export type SystemInfo = { + __typename?: 'SystemInfo'; + filename: Scalars['String']; + basename: Scalars['String']; + breadcrumbs: Array; + path: Scalars['String']; + relativePath: Scalars['String']; + extension: Scalars['String']; + template: Scalars['String']; + collection: Collection; +}; + + +export type SystemInfoBreadcrumbsArgs = { + excludeExtension?: InputMaybe; +}; + +export type PageInfo = { + __typename?: 'PageInfo'; + hasPreviousPage: Scalars['Boolean']; + hasNextPage: Scalars['Boolean']; + startCursor: Scalars['String']; + endCursor: Scalars['String']; +}; + +export type Node = { + id: Scalars['ID']; +}; + +export type Document = { + sys?: Maybe; + id: Scalars['ID']; + form: Scalars['JSON']; + values: Scalars['JSON']; +}; + +/** A relay-compliant pagination connection */ +export type Connection = { + totalCount: Scalars['Int']; +}; + +export type Query = { + __typename?: 'Query'; + getCollection: Collection; + getCollections: Array; + node: Node; + getDocument: DocumentNode; + getDocumentList: DocumentConnection; + getDocumentFields: Scalars['JSON']; + getPostsDocument: PostsDocument; + getPostsList: PostsConnection; +}; + + +export type QueryGetCollectionArgs = { + collection?: InputMaybe; +}; + + +export type QueryNodeArgs = { + id?: InputMaybe; +}; + + +export type QueryGetDocumentArgs = { + collection?: InputMaybe; + relativePath?: InputMaybe; +}; + + +export type QueryGetDocumentListArgs = { + before?: InputMaybe; + after?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +export type QueryGetPostsDocumentArgs = { + relativePath?: InputMaybe; +}; + + +export type QueryGetPostsListArgs = { + before?: InputMaybe; + after?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + +export type DocumentConnectionEdges = { + __typename?: 'DocumentConnectionEdges'; + cursor?: Maybe; + node?: Maybe; +}; + +export type DocumentConnection = Connection & { + __typename?: 'DocumentConnection'; + pageInfo?: Maybe; + totalCount: Scalars['Int']; + edges?: Maybe>>; +}; + +export type Collection = { + __typename?: 'Collection'; + name: Scalars['String']; + slug: Scalars['String']; + label?: Maybe; + path: Scalars['String']; + format?: Maybe; + matches?: Maybe; + templates?: Maybe>>; + fields?: Maybe>>; + documents: DocumentConnection; +}; + + +export type CollectionDocumentsArgs = { + before?: InputMaybe; + after?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + +export type DocumentNode = PostsDocument; + +export type Posts = { + __typename?: 'Posts'; + title?: Maybe; + description?: Maybe; + date?: Maybe; + tags?: Maybe; + imageUrl?: Maybe; + body?: Maybe; +}; + +export type PostsDocument = Node & Document & { + __typename?: 'PostsDocument'; + id: Scalars['ID']; + sys: SystemInfo; + data: Posts; + form: Scalars['JSON']; + values: Scalars['JSON']; + dataJSON: Scalars['JSON']; +}; + +export type PostsConnectionEdges = { + __typename?: 'PostsConnectionEdges'; + cursor?: Maybe; + node?: Maybe; +}; + +export type PostsConnection = Connection & { + __typename?: 'PostsConnection'; + pageInfo?: Maybe; + totalCount: Scalars['Int']; + edges?: Maybe>>; +}; + +export type Mutation = { + __typename?: 'Mutation'; + addPendingDocument: DocumentNode; + updateDocument: DocumentNode; + createDocument: DocumentNode; + updatePostsDocument: PostsDocument; + createPostsDocument: PostsDocument; +}; + + +export type MutationAddPendingDocumentArgs = { + collection: Scalars['String']; + relativePath: Scalars['String']; + template?: InputMaybe; +}; + + +export type MutationUpdateDocumentArgs = { + collection?: InputMaybe; + relativePath: Scalars['String']; + params: DocumentMutation; +}; + + +export type MutationCreateDocumentArgs = { + collection?: InputMaybe; + relativePath: Scalars['String']; + params: DocumentMutation; +}; + + +export type MutationUpdatePostsDocumentArgs = { + relativePath: Scalars['String']; + params: PostsMutation; +}; + + +export type MutationCreatePostsDocumentArgs = { + relativePath: Scalars['String']; + params: PostsMutation; +}; + +export type DocumentMutation = { + posts?: InputMaybe; +}; + +export type PostsMutation = { + title?: InputMaybe; + description?: InputMaybe; + date?: InputMaybe; + tags?: InputMaybe; + imageUrl?: InputMaybe; + body?: InputMaybe; +}; + +export type PostsPartsFragment = { __typename?: 'Posts', title?: string | null | undefined, description?: string | null | undefined, date?: string | null | undefined, tags?: string | null | undefined, imageUrl?: string | null | undefined, body?: any | null | undefined }; + +export type GetPostsDocumentQueryVariables = Exact<{ + relativePath: Scalars['String']; +}>; + + +export type GetPostsDocumentQuery = { __typename?: 'Query', getPostsDocument: { __typename?: 'PostsDocument', id: string, sys: { __typename?: 'SystemInfo', filename: string, basename: string, breadcrumbs: Array, path: string, relativePath: string, extension: string }, data: { __typename?: 'Posts', title?: string | null | undefined, description?: string | null | undefined, date?: string | null | undefined, tags?: string | null | undefined, imageUrl?: string | null | undefined, body?: any | null | undefined } } }; + +export type GetPostsListQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetPostsListQuery = { __typename?: 'Query', getPostsList: { __typename?: 'PostsConnection', totalCount: number, edges?: Array<{ __typename?: 'PostsConnectionEdges', node?: { __typename?: 'PostsDocument', id: string, sys: { __typename?: 'SystemInfo', filename: string, basename: string, breadcrumbs: Array, path: string, relativePath: string, extension: string }, data: { __typename?: 'Posts', title?: string | null | undefined, description?: string | null | undefined, date?: string | null | undefined, tags?: string | null | undefined, imageUrl?: string | null | undefined, body?: any | null | undefined } } | null | undefined } | null | undefined> | null | undefined } }; + +export const PostsPartsFragmentDoc = gql` + fragment PostsParts on Posts { + title + description + date + tags + imageUrl + body +} + `; +export const GetPostsDocumentDocument = gql` + query getPostsDocument($relativePath: String!) { + getPostsDocument(relativePath: $relativePath) { + sys { + filename + basename + breadcrumbs + path + relativePath + extension + } + id + data { + ...PostsParts + } + } +} + ${PostsPartsFragmentDoc}`; +export const GetPostsListDocument = gql` + query getPostsList { + getPostsList { + totalCount + edges { + node { + id + sys { + filename + basename + breadcrumbs + path + relativePath + extension + } + data { + ...PostsParts + } + } + } + } +} + ${PostsPartsFragmentDoc}`; +export type Requester = (doc: DocumentNode, vars?: V, options?: C) => Promise + export function getSdk(requester: Requester) { + return { + getPostsDocument(variables: GetPostsDocumentQueryVariables, options?: C): Promise<{data: GetPostsDocumentQuery, variables: GetPostsDocumentQueryVariables, query: string}> { + return requester<{data: GetPostsDocumentQuery, variables: GetPostsDocumentQueryVariables, query: string}, GetPostsDocumentQueryVariables>(GetPostsDocumentDocument, variables, options); + }, + getPostsList(variables?: GetPostsListQueryVariables, options?: C): Promise<{data: GetPostsListQuery, variables: GetPostsListQueryVariables, query: string}> { + return requester<{data: GetPostsListQuery, variables: GetPostsListQueryVariables, query: string}, GetPostsListQueryVariables>(GetPostsListDocument, variables, options); + } + }; + } + export type Sdk = ReturnType; + +// TinaSDK generated code +import { staticRequest } from 'tinacms' +const requester: (doc: any, vars?: any, options?: any) => Promise = async ( + doc, + vars, + _options +) => { + let data = {} + try { + data = await staticRequest({ + query: doc, + variables: vars, + }) + } catch (e) { + // swallow errors related to document creation + console.warn('Warning: There was an error when fetching data') + console.warn(e) + } + + return { data, query: doc, variables: vars || {} } +} + +/** + * @experimental this class can be used but may change in the future + **/ +export const ExperimentalGetTinaClient = ()=>getSdk(requester) + diff --git a/.tina/schema.ts b/.tina/schema.ts new file mode 100644 index 0000000..1a6c6cd --- /dev/null +++ b/.tina/schema.ts @@ -0,0 +1,143 @@ +import { defineSchema } from '@tinacms/cli'; + +export default defineSchema({ + collections: [ + { + label: 'Blog Posts', + name: 'posts', + path: 'posts', + fields: [ + { + type: 'string', + label: 'Title', + name: 'title', + }, + { + type: 'string', + label: 'Description', + name: 'description', + }, + { + type: 'string', + label: 'Date', + name: 'date', + }, + { + type: 'string', + label: 'Tags', + name: 'tags', + }, + { + type: 'string', + label: 'Image URL', + name: 'imageUrl', + }, + { + type: 'rich-text', + label: 'Blog Post Body', + name: 'body', + isBody: true, + templates: [ + { + name: 'Quote', + label: 'Quote', + fields: [ + { + type: 'string', + name: 'content', + label: 'Content', + }, + { + type: 'string', + name: 'author', + label: 'Author', + }, + { + type: 'string', + name: 'cite', + label: 'Cite', + }, + ], + }, + { + name: 'ArticleImage', + label: 'ArticleImage', + fields: [ + { + type: 'string', + name: 'src', + label: 'Src', + }, + { + type: 'string', + name: 'caption', + label: 'Caption', + }, + ], + }, + { + name: 'Code', + label: 'Code', + fields: [ + { + type: 'string', + name: 'code', + label: 'Code', + }, + { + type: 'string', + name: 'language', + label: 'Language', + }, + { + type: 'string', + name: 'selectedLines', + label: 'Selected Lines', + }, + { + type: 'boolean', + name: 'withCopyButton', + label: 'With Copy Button', + }, + { + type: 'boolean', + name: 'withLineNumbers', + label: 'With Line Numbers', + }, + { + type: 'string', + name: 'caption', + label: 'Caption', + }, + ], + }, + { + name: 'h2', + label: 'H2', + inline: true, + fields: [], + }, + { + name: 'h3', + label: 'H3', + inline: true, + fields: [], + }, + { + name: 'br', + label: 'BR', + inline: true, + fields: [], + }, + { + name: 'p', + label: 'P', + inline: true, + fields: [], + }, + ], + }, + ], + }, + ], +}); diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3fe865a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..144e1d2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Blazity + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1eb917e --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +
+

+ + + Logo + + + Logo + + +

✨ Free Next.js marketing website template for SaaS startups ✨

+ +

+ Everything you need to build a great landing page / marketing website for your startup. Great SEO metrics, Green WebVitals, 🚀 Performance, Clean & Pragmatic Codebase out of the box. +
+
+ View Demo + . + Report Bug + . + Request Feature +

+

+ +
+ +![Contributors](https://img.shields.io/github/contributors/Blazity/next-saas-starter?color=dark-green) ![Issues](https://img.shields.io/github/issues/Blazity/next-saas-starter) ![License](https://img.shields.io/github/license/Blazity/next-saas-starter) + +
+ +

Created with :heart: at Blazity

+

Blazity is a group of Next.js/Jamstack/Headless experts. Contact us at contact@blazity.com if you’d like to talk about your project or just to have a chat with us

+ + + Blazity Discord Banner + +
+ +## Table Of Contents + +- [Features](#-features) +- [Getting Started](#-getting-started) +- [One click deploy](#one-click-deploy) +- [Built With](#-built-with) +- [Contributing](#-contributing) + - [Creating A Pull Request](#creating-a-pull-request) +- [Acknowledgements](#-acknowledgements) +- [Contributors](#-contributors) +- [License](#-license) + +## Features + +- ⚡ **Next.js** - React framework for static rendering +- **Best SEO setup** - Meta Tags, JSON-LD and Open Graph Tags +- **[Tina CMS](https://tina.io/) integration** - local & (optional) production CMS +- **Optimized for Web Vitals** +- **Blog with MDX** +- **Mailchimp Integration** - for newsletters +- **Sendgrid Integration** - for sending emails +- **Dark mode** - and customizable themes! +- **No UI library** - just styled components, so you don't have to learn any new syntax +- **One click deployment** - with Vercel or any other serverless deployment environment +- **Eslint** - with Next.js's recommended settings and imports sorting rule +- **Prettier** + +## 🤓 Getting Started + +- Click `Use the template` or [this link](https://github.com/Blazity/next-saas-starter/generate) +- Setup your [sendgrid](https://sendgrid.com/) API key and add it to environment variables (`SENDGRID_API_KEY` - `.env.local`) +- Adjust the template to your needs (and checkout `env.ts` file) +- Deploy the project on [Vercel](https://vercel.com/) **don't forget to add env variables** +- _(optional)_ Create [Tina Cloud account](https://app.tina.io/), [a project](https://tina.io/docs/tina-cloud/) and fill these `NEXT_PUBLIC_ORGANIZATION_NAME`, `NEXT_PUBLIC_TINA_CLIENT_ID` env vars with proper values + > Tina's Content API authenticates directly with GitHub removing the need for users to create GitHub accounts. Access is granted through the dashboard, allowing users to login directly through your site and begin editing! Any changes that are saved by your editors will be commited to the configured branch in your GitHub repository. + - For more details [see the docs](https://tina.io/docs/tina-cloud/) + +``` +# run the dev mode +$ yarn dev + +# run the prod mode +yarn start + +# build the app +yarn build +``` + +> Hint: To edit the blog pages go to [/admin](http://localhost:3000/admin) and navigate to a blog page to edit it. To exit editing mode navigate to [/admin/logout](http://localhost:3000/admin/logout) + +## 🚀 One click deploy + +Clone the repository and one-click deploy to Vercel for free! + +[![Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/Blazity/next-saas-starter) + +Clone the repository and one-click deploy to Netlify for free! + +[![Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/Blazity/next-saas-starter) + +## 🧰 Built With + +- Statically generated pages with [**Next.js** ](https://github.com/vercel/next.js) +- [Styled components](https://github.com/styled-components/styled-components/) +- [MDX](https://github.com/mdx-js/mdx) +- [TypeScript](https://github.com/Microsoft/TypeScript) + +## 🤲🏻 Contributing + +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +- If you have suggestions for adding or removing projects, feel free to [open an issue](https://github.com/Blazity/next-saas-starter/issues/new) to discuss it, or directly create a pull request after you edit the _README.md_ file with necessary changes. +- Create individual PR for each suggestion. + +### Creating A Pull Request + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## 😎 Acknowledgements + +Big thanks to authors of these libraries: + +- https://github.com/neg4n/next-api-og-image - generating open graph images +- https://github.com/blazity/nextjs-color-mode - non-flickering dark mode +- https://github.com/Brew-Brew/css-in-js-media - a convenient way of creating media queries + +## Support + +If you're looking for help or simply want to share your thoughts about the project, we encourage you to join our Discord community. Here's the link: [https://blazity.com/discord](https://blazity.com/discord). It's a space where we exchange ideas and help one another. Everyone's input is appreciated, and we look forward to welcoming you. + +## ✨ Contributors + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + +

Bart Stefanski

💻

Igor Lasota

💻

Jan Bryński

💻

Logan Anderson

💻 📖 🧑‍🏫
+ + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! + +## 📝 License + +Distributed under the MIT License. See [LICENSE](https://github.com/Blazity/next-saas-starter/blob/main/LICENSE.md) for more information. diff --git a/components/Accordion.tsx b/components/Accordion.tsx new file mode 100644 index 0000000..4c50e63 --- /dev/null +++ b/components/Accordion.tsx @@ -0,0 +1,81 @@ +import { PropsWithChildren, useState } from 'react'; +import styled from 'styled-components'; +import { media } from 'utils/media'; +import Collapse from './Collapse'; +import RichText from './RichText'; + +interface AccordionProps { + title: string; + isOpen?: boolean; +} + +export default function Accordion({ title, isOpen, children }: PropsWithChildren) { + const [hasCollapsed, setHasCollapsed] = useState(!isOpen); + const isActive = !hasCollapsed; + return ( + setHasCollapsed((prev) => !prev)}> + + {title} + + + + + + + {children} + + + + ); +} + +const Title = styled.h3` + font-size: 2rem; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; +`; + +const TitleWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const Icon = styled.div<{ isActive: boolean }>` + width: 2.4rem; + transition: transform 0.3s; + transform: rotateZ(${(p) => (p.isActive ? 180 : 0)}deg); +`; + +const Description = styled.div` + margin-top: 2.5rem; + font-size: 1.6rem; + font-weight: normal; +`; + +const AccordionWrapper = styled.div` + display: flex; + flex-direction: column; + padding: 2rem 1.5rem; + background: rgb(var(--cardBackground)); + box-shadow: var(--shadow-md); + cursor: pointer; + border-radius: 0.6rem; + transition: opacity 0.2s; + + ${media('<=desktop')} { + width: 100%; + } +`; diff --git a/components/ArticleCard.tsx b/components/ArticleCard.tsx new file mode 100644 index 0000000..353daa6 --- /dev/null +++ b/components/ArticleCard.tsx @@ -0,0 +1,107 @@ +import NextImage from 'next/image'; +import NextLink from 'next/link'; +import styled from 'styled-components'; +import { media } from 'utils/media'; + +export interface ArticleCardProps { + title: string; + slug: string; + imageUrl: string; + description: string; +} + +export default function ArticleCard({ title, slug, imageUrl, description }: ArticleCardProps) { + return ( + + + + + + + + {title} + {description} + + + + + ); +} + +const ArticleCardWrapper = styled.a` + display: flex; + flex-direction: column; + height: 45rem; + max-width: 35rem; + overflow: hidden; + text-decoration: none; + border-radius: 0.6rem; + background: rgb(var(--cardBackground)); + cursor: pointer; + color: rgb(var(--text)); +`; + +const HoverEffectContainer = styled.div` + transition: transform 0.3s; + backface-visibility: hidden; + will-change: transform; + + &:hover { + border-radius: 0.6rem; + overflow: hidden; + transform: scale(1.025); + } +`; + +const ImageContainer = styled.div` + position: relative; + height: 20rem; + + &:before { + display: block; + content: ''; + width: 100%; + padding-top: calc((9 / 16) * 100%); + } + + & > div { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + ${media('<=desktop')} { + width: 100%; + } +`; + +const Content = styled.div` + padding: 0 2rem; + + & > * { + margin-top: 2rem; + } +`; + +const Title = styled.h4` + font-size: 1.8rem; + + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +`; + +const Description = styled.p` + font-size: 1.6rem; + + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + opacity: 0.6; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; +`; diff --git a/components/ArticleImage.tsx b/components/ArticleImage.tsx new file mode 100644 index 0000000..4e4feb7 --- /dev/null +++ b/components/ArticleImage.tsx @@ -0,0 +1,63 @@ +import NextImage, { ImageProps } from 'next/image'; +import React from 'react'; +import styled from 'styled-components'; + +interface ArticleImageProps extends ImageProps { + src: string; + caption?: string; +} + +export default function ArticleImage({ src, caption, ...rest }: ArticleImageProps) { + return ( + + + + + {caption} + + ); +} + +const ImageWrapper = styled.div` + position: relative; + max-width: 90rem; + border-radius: 0.6rem; + overflow: hidden; + + &::before { + float: left; + padding-top: 56.25%; + content: ''; + } + + &::after { + display: block; + content: ''; + clear: both; + } +`; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + + &:not(:last-child) { + margin-bottom: 3rem; + } +`; + +const Caption = styled.small` + display: block; + font-size: 1.4rem; + text-align: center; + margin-top: 1rem; +`; diff --git a/components/AutofitGrid.tsx b/components/AutofitGrid.tsx new file mode 100644 index 0000000..d4e3eeb --- /dev/null +++ b/components/AutofitGrid.tsx @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +const AutofitGrid = styled.div` + --autofit-grid-item-size: 30rem; + + display: grid; + grid-gap: 2rem; + grid-template-columns: repeat(auto-fit, minmax(var(--autofit-grid-item-size), 1fr)); + margin: 0 auto; +`; + +export default AutofitGrid; diff --git a/components/BasicCard.tsx b/components/BasicCard.tsx new file mode 100644 index 0000000..0a8e416 --- /dev/null +++ b/components/BasicCard.tsx @@ -0,0 +1,45 @@ +import NextImage from 'next/image'; +import styled from 'styled-components'; + +interface BasicCardProps { + title: string; + description: string; + imageUrl: string; +} + +export default function BasicCard({ title, description, imageUrl }: BasicCardProps) { + return ( + + + {title} + {description} + + ); +} + +const Card = styled.div` + display: flex; + padding: 2.5rem; + background: rgb(var(--cardBackground)); + box-shadow: var(--shadow-md); + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + width: 100%; + border-radius: 0.6rem; + color: rgb(var(--text)); + font-size: 1.6rem; + + & > *:not(:first-child) { + margin-top: 1rem; + } +`; + +const Title = styled.div` + font-weight: bold; +`; + +const Description = styled.div` + opacity: 0.6; +`; diff --git a/components/BasicSection.tsx b/components/BasicSection.tsx new file mode 100644 index 0000000..fe6c746 --- /dev/null +++ b/components/BasicSection.tsx @@ -0,0 +1,93 @@ +import NextImage from 'next/image'; +import React, { PropsWithChildren } from 'react'; +import styled from 'styled-components'; +import { media } from 'utils/media'; +import Container from './Container'; +import OverTitle from './OverTitle'; +import RichText from './RichText'; + +export interface BasicSectionProps { + imageUrl: string; + title: string; + overTitle: string; + reversed?: boolean; +} + +export default function BasicSection({ imageUrl, title, overTitle, reversed, children }: PropsWithChildren) { + return ( + + + + + + {overTitle} + {title} + {children} + + + ); +} + +const Title = styled.h1` + font-size: 5.2rem; + font-weight: bold; + line-height: 1.1; + margin-bottom: 4rem; + letter-spacing: -0.03em; + + ${media('<=tablet')} { + font-size: 4.6rem; + margin-bottom: 2rem; + } +`; + +const CustomOverTitle = styled(OverTitle)` + margin-bottom: 2rem; +`; + +const ImageContainer = styled.div` + flex: 1; + + position: relative; + &:before { + display: block; + content: ''; + width: 100%; + padding-top: calc((9 / 16) * 100%); + } + + & > div { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + ${media('<=desktop')} { + width: 100%; + } +`; + +const ContentContainer = styled.div` + flex: 1; +`; + +type Props = Pick; +const BasicSectionWrapper = styled(Container)` + display: flex; + align-items: center; + flex-direction: ${(p: Props) => (p.reversed ? 'row-reverse' : 'row')}; + + ${ImageContainer} { + margin: ${(p: Props) => (p.reversed ? '0 0 0 5rem' : '0 5rem 0 0')}; + } + + ${media('<=desktop')} { + flex-direction: column; + + ${ImageContainer} { + margin: 0 0 2.5rem 0; + } + } +`; diff --git a/components/Button.tsx b/components/Button.tsx new file mode 100644 index 0000000..09194a9 --- /dev/null +++ b/components/Button.tsx @@ -0,0 +1,35 @@ +import { PropsWithChildren } from 'react'; +import styled from 'styled-components'; + +type ButtonProps = PropsWithChildren<{ transparent?: boolean }>; + +const Button = styled.a` + border: none; + background: none; + display: inline-block; + text-decoration: none; + text-align: center; + background: ${(p) => (p.transparent ? 'transparent' : 'rgb(var(--primary))')}; + padding: 1.75rem 2.25rem; + font-size: 1.2rem; + color: ${(p) => (p.transparent ? 'rgb(var(--text))' : 'rgb(var(--textSecondary))')}; + text-transform: uppercase; + font-family: var(--font); + font-weight: bold; + border-radius: 0.4rem; + border: ${(p) => (p.transparent ? 'none' : '2px solid rgb(var(--primary))')}; + transition: transform 0.3s; + backface-visibility: hidden; + will-change: transform; + cursor: pointer; + + span { + margin-left: 2rem; + } + + &:hover { + transform: scale(1.025); + } +`; + +export default Button; diff --git a/components/ButtonGroup.tsx b/components/ButtonGroup.tsx new file mode 100644 index 0000000..a80f18b --- /dev/null +++ b/components/ButtonGroup.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components'; +import { media } from 'utils/media'; + +const ButtonGroup = styled.div` + display: flex; + flex-wrap: wrap; + + & > *:not(:last-child) { + margin-right: 2rem; + } + + ${media('<=tablet')} { + & > * { + width: 100%; + } + + & > *:not(:last-child) { + margin-bottom: 2rem; + margin-right: 0rem; + } + } +`; + +export default ButtonGroup; diff --git a/components/ClientOnly.tsx b/components/ClientOnly.tsx new file mode 100644 index 0000000..bf939d7 --- /dev/null +++ b/components/ClientOnly.tsx @@ -0,0 +1,11 @@ +import { PropsWithChildren, useEffect, useState } from 'react' + +export default function ClientOnly(props: PropsWithChildren) { + const { children, ...rest } = props + const [hasMounted, setHasMounted] = useState(false) + useEffect(() => { + setHasMounted(true) + }, []) + if (!hasMounted) return
+ return <>{props.children} +} diff --git a/components/CloseIcon.tsx b/components/CloseIcon.tsx new file mode 100644 index 0000000..6b464bd --- /dev/null +++ b/components/CloseIcon.tsx @@ -0,0 +1,14 @@ +import Icon, { IconProps } from './Icon' + +export default function CloseIcon(props: IconProps) { + return ( + + + + ) +} diff --git a/components/Code.tsx b/components/Code.tsx new file mode 100644 index 0000000..3416dbd --- /dev/null +++ b/components/Code.tsx @@ -0,0 +1,185 @@ +import Highlight, { defaultProps, Language } from 'prism-react-renderer'; +import React from 'react'; +import styled from 'styled-components'; +import ClientOnly from 'components/ClientOnly'; +import { useClipboard } from 'hooks/useClipboard'; + +export interface CodeProps { + code: string; + language?: Language; + selectedLines?: number[]; + withCopyButton?: boolean; + withLineNumbers?: boolean; + caption?: string; +} + +export default function Code({ + code, + language = 'javascript', + selectedLines = [], + withCopyButton = true, + withLineNumbers, + caption, +}: CodeProps) { + const { copy, copied } = useClipboard({ + copiedTimeout: 600, + }); + + function handleCopyClick(code: string) { + copy(code); + } + + const copyButtonMarkup = ( + + handleCopyClick(code)}> + + + + ); + + return ( + <> + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( + <> + + {withCopyButton && copyButtonMarkup} +
+                {tokens.map((line, i) => {
+                  const lineNumber = i + 1;
+                  const isSelected = selectedLines.includes(lineNumber);
+                  const lineProps = getLineProps({ line, key: i });
+                  const className = lineProps.className + (isSelected ? ' selected-line' : '');
+
+                  return (
+                    
+                      {withLineNumbers && {lineNumber}}
+                      
+                        {line.map((token, key) => (
+                          
+                        ))}
+                      
+                    
+                  );
+                })}
+              
+
+ {caption && {caption}} + + )} +
+ + ); +} + +function CopyIcon() { + return ( + + + + ); +} + +const Caption = styled.small` + position: relative; + top: -2.2rem; + word-break: break-word; + font-size: 1.2rem; +`; + +const CopyButton = styled.button<{ copied: boolean }>` + position: absolute; + border: none; + top: 2.4rem; + right: 2.4rem; + visibility: hidden; + background-color: rgba(var(--secondary), 0.1); + cursor: pointer; + width: 3rem; + height: 3rem; + line-height: normal; + border-radius: 0.3rem; + color: rgb(var(--text)); + z-index: 1; + line-height: 1; + + &::after { + position: absolute; + content: 'Copied'; + visibility: ${(p) => (p.copied ? 'visible' : 'hidden')}; + top: 0; + left: -4rem; + height: 3rem; + font-weight: bold; + border-radius: 0.3rem; + line-height: 1.5; + font-size: 1.4rem; + padding: 0.5rem 1rem; + color: rgb(var(--primary)); + background-color: rgb(var(--secondary)); + } + + &:hover { + background-color: rgba(var(--secondary), 0.2); + } +`; + +const CodeWrapper = styled.div<{ language: string }>` + position: relative; + border-radius: 0.3em; + margin-top: 4.5rem; + transition: visibility 0.1s; + font-size: 1.6rem; + + &:not(:last-child) { + margin-bottom: 3rem; + } + + &::after { + position: absolute; + height: 2.2em; + content: '${(p) => p.language}'; + right: 2.4rem; + padding: 1.2rem; + top: -2em; + line-height: 1rem; + border-radius: 0.3em; + font-size: 1.5rem; + text-transform: uppercase; + background-color: inherit; + font-weight: bold; + text-align: center; + } + + &:hover { + ${CopyButton} { + visibility: visible; + } + } +`; + +const Pre = styled.pre` + text-align: left; + margin: 1em 0; + padding: 0.5em; + overflow: scroll; +`; + +const Line = styled.div` + display: flex; +`; + +const LineNo = styled.span` + display: table-cell; + text-align: right; + padding-right: 1em; + user-select: none; + opacity: 0.5; +`; + +const LineContent = styled.span` + display: table-cell; +`; diff --git a/components/Collapse.tsx b/components/Collapse.tsx new file mode 100644 index 0000000..d311363 --- /dev/null +++ b/components/Collapse.tsx @@ -0,0 +1,49 @@ +import { forwardRef, PropsWithChildren } from 'react'; +import AnimateHeight from 'react-animate-height'; + +export interface CollapseProps { + isOpen?: boolean; + animateOpacity?: boolean; + onAnimationStart?: () => void; + onAnimationEnd?: () => void; + duration?: number; + easing?: string; + startingHeight?: number | string; + endingHeight?: number | string; +} + +const Collapse = forwardRef>( + ( + { + isOpen, + animateOpacity = true, + onAnimationStart, + onAnimationEnd, + duration, + easing = 'ease', + startingHeight = 0, + endingHeight = 'auto', + ...rest + }, + ref, + ) => { + return ( + +
+ + ); + }, +); + +export default Collapse; diff --git a/components/ColorSwitcher.tsx b/components/ColorSwitcher.tsx new file mode 100644 index 0000000..363288f --- /dev/null +++ b/components/ColorSwitcher.tsx @@ -0,0 +1,47 @@ +import { ColorModeStyles, useColorModeValue, useColorSwitcher } from 'nextjs-color-mode'; +import styled from 'styled-components'; + +export default function ColorSwitcher() { + const { toggleTheme, colorMode } = useColorSwitcher(); + + const sunIcon = ( + + + + + + + + + + + + + + ); + + const moonIcon = ( + + + + ); + + return {colorMode === 'light' ? moonIcon : sunIcon}; +} + +const CustomButton = styled.button` + display: flex; + cursor: pointer; + align-items: center; + border: 0; + width: 4rem; + height: 4rem; + background: transparent; + + svg { + color: var(--logoColor); + } +`; diff --git a/components/Container.tsx b/components/Container.tsx new file mode 100644 index 0000000..3da45d7 --- /dev/null +++ b/components/Container.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +const Container = styled.div` + position: relative; + max-width: 130em; + width: 100%; + margin: 0 auto; + padding: 0 2rem; +`; + +export default Container; diff --git a/components/Drawer.tsx b/components/Drawer.tsx new file mode 100644 index 0000000..bd626b7 --- /dev/null +++ b/components/Drawer.tsx @@ -0,0 +1,3 @@ +import * as OriginalDrawer from '@accessible/drawer' + +export default OriginalDrawer diff --git a/components/Footer.tsx b/components/Footer.tsx new file mode 100644 index 0000000..69a30f9 --- /dev/null +++ b/components/Footer.tsx @@ -0,0 +1,176 @@ +import NextLink from 'next/link'; +import { FacebookIcon, LinkedinIcon, TwitterIcon } from 'react-share'; +import styled from 'styled-components'; +import Container from 'components/Container'; +import { media } from 'utils/media'; + +type SingleFooterListItem = { title: string; href: string }; +type FooterListItems = SingleFooterListItem[]; +type SingleFooterList = { title: string; items: FooterListItems }; +type FooterItems = SingleFooterList[]; + +const footerItems: FooterItems = [ + { + title: 'Company', + items: [ + { title: 'Privacy Policy', href: '/privacy-policy' }, + { title: 'Cookies Policy', href: '/cookies-policy' }, + ], + }, + { + title: 'Product', + items: [ + { title: 'Features', href: '/features' }, + { title: 'Something', href: '/something' }, + { title: 'Something else', href: '/something-else' }, + { title: 'And something else', href: '/and-something-else' }, + ], + }, + { + title: 'Knowledge', + items: [ + { title: 'Blog', href: '/blog' }, + { title: 'Contact', href: '/contact' }, + { title: 'FAQ', href: '/faq' }, + { title: 'Help Center', href: '/help-center' }, + ], + }, + { + title: 'Something', + items: [ + { title: 'Features2', href: '/features2' }, + { title: 'Something2', href: '/something2' }, + { title: 'Something else2', href: '/something-else2' }, + { title: 'And something else2', href: '/and-something-else2' }, + ], + }, +]; + +export default function Footer() { + return ( + + + + {footerItems.map((singleItem) => ( + + ))} + + + + + + + + + + + + + + + + + + + + + + © Copyright 2021 My Saas Startup + + + + ); +} + +function FooterList({ title, items }: SingleFooterList) { + return ( + + {title} + {items.map((singleItem) => ( + + ))} + + ); +} + +function ListItem({ title, href }: SingleFooterListItem) { + return ( + + + {title} + + + ); +} + +const FooterWrapper = styled.div` + padding-top: 10rem; + padding-bottom: 4rem; + background: rgb(var(--secondary)); + color: rgb(var(--textSecondary)); +`; + +const ListContainer = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; +`; + +const ListHeader = styled.p` + font-weight: bold; + font-size: 2.25rem; + margin-bottom: 2.5rem; +`; + +const ListWrapper = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 5rem; + margin-right: 5rem; + + & > *:not(:first-child) { + margin-top: 1rem; + } + + ${media('<=tablet')} { + flex: 0 40%; + margin-right: 1.5rem; + } + + ${media('<=phone')} { + flex: 0 100%; + margin-right: 0rem; + } +`; + +const ListItemWrapper = styled.p` + font-size: 1.6rem; + + a { + text-decoration: none; + color: rgba(var(--textSecondary), 0.75); + } +`; + +const ShareBar = styled.div` + & > *:not(:first-child) { + margin-left: 1rem; + } +`; + +const Copyright = styled.p` + font-size: 1.5rem; + margin-top: 0.5rem; +`; + +const BottomBar = styled.div` + margin-top: 6rem; + display: flex; + justify-content: space-between; + align-items: center; + + ${media('<=tablet')} { + flex-direction: column; + } +`; diff --git a/components/GlobalStyles.tsx b/components/GlobalStyles.tsx new file mode 100644 index 0000000..f5ccc50 --- /dev/null +++ b/components/GlobalStyles.tsx @@ -0,0 +1,161 @@ +import { createGlobalStyle } from 'styled-components'; + +// default breakpoints +// { +// smallPhone: 320; +// phone: 375; +// tablet: 768; +// desktop: 1024; +// largeDesktop: 1440; +// } + +export const GlobalStyle = createGlobalStyle` + +.next-light-theme { + --background: 251,251,253; + --secondBackground: 255,255,255; + --text: 10,18,30; + --textSecondary: 255,255,255; + --primary: 22,115,255; + --secondary: 10,18,30; + --tertiary: 231,241,251; + --cardBackground: 255,255,255; + --inputBackground: 255,255,255; + --navbarBackground: 255,255,255; + --modalBackground: 251,251,253; + --errorColor: 207,34,46; + --logoColor: #243A5A; +} + +.next-dark-theme { + --background: 26,32,44; + --secondBackground: 45,55,72; + --text: 237,237,238; + --textSecondary: 255,255,255; + --primary: 22,115,255; + --secondary: 10,18,30; + --tertiary: 231,241,251; + --cardBackground: 45,55,72; + --inputBackground: 45,55,72; + --navbarBackground: 45,55,72; + --modalBackground: 26,32,44; + --errorColor: 207,34,46; + --logoColor: #fff; +} + +:root { + --font: 'Poppins', sans-serif; + + --shadow-md: 0 2px 4px 0 rgb(12 0 46 / 4%); + --shadow-lg: 0 10px 14px 0 rgb(12 0 46 / 6%); + + --z-sticky: 7777; + --z-navbar: 8888; + --z-drawer: 9999; + --z-modal: 9999; +} + +/* Box sizing rules */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* Remove default margin */ +body, +h1, +h2, +h3, +h4, +p, +figure, +blockquote, +dl, +dd { + margin: 0; +} + + +/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ +ul[role='list'], +ol[role='list'] { + list-style: none; +} + +/* Set core root defaults */ +html:focus-within { + scroll-behavior: smooth; +} + +html { + -webkit-font-smoothing: antialiased; + touch-action: manipulation; + text-rendering: optimizelegibility; + text-size-adjust: 100%; + font-size: 62.5%; + + @media (max-width: 37.5em) { + font-size: 50%; + } + + @media (max-width: 48.0625em) { + font-size: 55%; + } + + @media (max-width: 56.25em) { + font-size: 60%; + } +} + +/* Set core body defaults */ +body { + min-height: 100vh; + text-rendering: optimizeSpeed; + line-height: 1.5; + font-family: var(--font); + color: rgb(var(--text)); + background: rgb(var(--background)); + font-feature-settings: "kern"; +} + +svg { + color: rgb(var(--text)); +} + +/* A elements that don't have a class get default styles */ +a:not([class]) { + text-decoration-skip-ink: auto; +} + +/* Make images easier to work with */ +img, +picture { + max-width: 100%; + display: block; +} + +/* Inherit fonts for inputs and buttons */ +input, +button, +textarea, +select { + font: inherit; +} + +/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ +@media (prefers-reduced-motion: reduce) { + html:focus-within { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + +}`; diff --git a/components/HamburgerIcon.tsx b/components/HamburgerIcon.tsx new file mode 100644 index 0000000..f6e9c1f --- /dev/null +++ b/components/HamburgerIcon.tsx @@ -0,0 +1,19 @@ +import Icon, { IconProps } from './Icon' + +export function HamburgerIcon(props: IconProps) { + return ( + + + + ) +} diff --git a/components/HeroIllustation.tsx b/components/HeroIllustation.tsx new file mode 100644 index 0000000..be15f81 --- /dev/null +++ b/components/HeroIllustation.tsx @@ -0,0 +1,769 @@ +export default function HeroIllustration() { + return ( + + + + + + + + + startup life + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/components/Icon.tsx b/components/Icon.tsx new file mode 100644 index 0000000..c6532e1 --- /dev/null +++ b/components/Icon.tsx @@ -0,0 +1,14 @@ +import React, { HTMLProps, Ref } from 'react'; +import styled from 'styled-components'; + +export type IconProps = HTMLProps & { _ref?: Ref }; + +export default function Icon({ _ref, ...rest }: any) { + return ; +} + +const IconWrapper = styled.button` + border: none; + background-color: transparent; + width: 4rem; +`; diff --git a/components/Input.tsx b/components/Input.tsx new file mode 100644 index 0000000..b116073 --- /dev/null +++ b/components/Input.tsx @@ -0,0 +1,18 @@ +import styled from 'styled-components'; + +const Input = styled.input` + border: 1px solid rgb(var(--inputBackground)); + background: rgb(var(--inputBackground)); + border-radius: 0.6rem; + font-size: 1.6rem; + padding: 1.8rem; + box-shadow: var(--shadow-md); + /* color: rgb(var(--textSecondary)); */ + + &:focus { + outline: none; + box-shadow: var(--shadow-lg); + } +`; + +export default Input; diff --git a/components/Link.tsx b/components/Link.tsx new file mode 100644 index 0000000..ec091c2 --- /dev/null +++ b/components/Link.tsx @@ -0,0 +1,40 @@ +import NextLink from 'next/link'; +import { PropsWithChildren } from 'react'; +import styled from 'styled-components'; + +export interface LinkProps { + href: string; +} + +export default function Link({ href, children }: PropsWithChildren) { + return ( + + {children} + + ); +} + +const Anchor = styled.a` + display: inline; + width: fit-content; + text-decoration: none; + + background: linear-gradient(rgb(var(--primary)), rgb(var(--primary))); + background-position: 0% 100%; + background-repeat: no-repeat; + background-size: 100% 0px; + transition: 100ms; + transition-property: background-size, text-decoration, color; + color: rgb(var(--primary)); + + &:hover { + background-size: 100% 100%; + text-decoration: none; + color: rgb(var(--background)); + } + + &:active { + color: rgb(var(--background)); + background-size: 100% 100%; + } +`; diff --git a/components/Logo.tsx b/components/Logo.tsx new file mode 100644 index 0000000..35684cf --- /dev/null +++ b/components/Logo.tsx @@ -0,0 +1,67 @@ +export default function Logo({ ...rest }) { + return ( + + + {' '} + {' '} + {' '} + {' '} + {' '} + {' '} + {' '} + {' '} + {' '} + {' '} + {' '} + + + ); +} diff --git a/components/MDXRichText.tsx b/components/MDXRichText.tsx new file mode 100644 index 0000000..75a67dc --- /dev/null +++ b/components/MDXRichText.tsx @@ -0,0 +1,117 @@ +import styled from 'styled-components'; +import { Components, TinaMarkdown, TinaMarkdownContent } from 'tinacms/dist/rich-text'; +import { media } from 'utils/media'; +import ArticleImage from './ArticleImage'; +import Code from './Code'; +import Link from './Link'; +import Quote from './Quote'; + +export default function RichText(props: { content: TinaMarkdownContent | TinaMarkdownContent[] }) { + return ( + + } /> + + ); +} + +const Container = styled.div` + display: flex; + ${'' /* Opting-out of margin-collapse */} + + flex-direction: column; + width: 100%; + + section:not(:last-child) { + margin-bottom: 3.8rem; + } + + a { + word-break: break-word; + } + + ${media('<=desktop')} { + .remark-highlight { + width: 100%; + overflow-x: auto; + } + } + + & > section, + .footnotes { + ${'' /* content-visibility: auto; */} + } + + ol, + ul { + font-size: 1.8rem; + line-height: 2.7rem; + margin: 0; + padding-left: 2.4rem; + li { + & > * { + vertical-align: top; + } + } + + &:not(:last-child) { + margin-bottom: 2.7rem; + } + } +`; + +const Paragraph = styled.p` + font-size: 1.8rem; + line-height: 2.7rem; + hanging-punctuation: first; + + &:not(:last-child) { + margin-bottom: 2.7rem; + } + + & + ul, + & + li { + margin-top: -1.5rem !important; + } +`; + +const SecondHeading = styled.h2` + font-size: 2.5rem; + line-height: 3.75rem; + margin-bottom: 3.75rem; +`; + +const ThirdHeading = styled.h3` + font-size: 2.2rem; + line-height: 3.4rem; + margin-bottom: 3.4rem; +`; + +const Break = styled.br` + display: block; + content: ''; + margin: 0; + height: 3rem; +`; + +const TextHighlight = styled.code` + display: inline-block; + padding: 0 0.6rem; + color: rgb(var(--textSecondary)); + border-radius: 0.4rem; + background-color: rgba(var(--primary), 0.8); + font-size: 1.6rem; + font-family: inherit; +`; + +const components = { + h2: SecondHeading, + h3: ThirdHeading, + p: Paragraph, + br: Break, + inlineCode: TextHighlight, + Image: ArticleImage, + Link, + Code, + Quote, + ArticleImage, +}; diff --git a/components/MailSentState.tsx b/components/MailSentState.tsx new file mode 100644 index 0000000..71319b3 --- /dev/null +++ b/components/MailSentState.tsx @@ -0,0 +1,65 @@ +import styled from 'styled-components'; + +export default function MailSentState() { + return ( + + + + + + + + + + + +

Mail successfully sent!

+
+ ); +} + +const Wrapper = styled.div` + flex: 1; + + & > *:not(:first-child) { + margin-top: 5rem; + } + + svg { + width: 100%; + height: 25rem; + } + + p { + font-size: 2.5rem; + text-align: center; + } +`; diff --git a/components/Navbar.tsx b/components/Navbar.tsx new file mode 100644 index 0000000..9e276fa --- /dev/null +++ b/components/Navbar.tsx @@ -0,0 +1,193 @@ +import dynamic from 'next/dynamic'; +import NextLink from 'next/link'; +import { useRouter } from 'next/router'; +import React, { useRef, useState } from 'react'; +import styled from 'styled-components'; +import { useNewsletterModalContext } from 'contexts/newsletter-modal.context'; +import { ScrollPositionEffectProps, useScrollPosition } from 'hooks/useScrollPosition'; +import { NavItems, SingleNavItem } from 'types'; +import { media } from 'utils/media'; +import Button from './Button'; +import Container from './Container'; +import Drawer from './Drawer'; +import { HamburgerIcon } from './HamburgerIcon'; +import Logo from './Logo'; + +const ColorSwitcher = dynamic(() => import('../components/ColorSwitcher'), { ssr: false }); + +type NavbarProps = { items: NavItems }; +type ScrollingDirections = 'up' | 'down' | 'none'; +type NavbarContainerProps = { hidden: boolean; transparent: boolean }; + +export default function Navbar({ items }: NavbarProps) { + const router = useRouter(); + const { toggle } = Drawer.useDrawer(); + const [scrollingDirection, setScrollingDirection] = useState('none'); + + let lastScrollY = useRef(0); + const lastRoute = useRef(''); + const stepSize = useRef(50); + + useScrollPosition(scrollPositionCallback, [router.asPath], undefined, undefined, 50); + + function scrollPositionCallback({ currPos }: ScrollPositionEffectProps) { + const routerPath = router.asPath; + const hasRouteChanged = routerPath !== lastRoute.current; + + if (hasRouteChanged) { + lastRoute.current = routerPath; + setScrollingDirection('none'); + return; + } + + const currentScrollY = currPos.y; + const isScrollingUp = currentScrollY > lastScrollY.current; + const scrollDifference = Math.abs(lastScrollY.current - currentScrollY); + const hasScrolledWholeStep = scrollDifference >= stepSize.current; + const isInNonCollapsibleArea = lastScrollY.current > -50; + + if (isInNonCollapsibleArea) { + setScrollingDirection('none'); + lastScrollY.current = currentScrollY; + return; + } + + if (!hasScrolledWholeStep) { + lastScrollY.current = currentScrollY; + return; + } + + setScrollingDirection(isScrollingUp ? 'up' : 'down'); + lastScrollY.current = currentScrollY; + } + + const isNavbarHidden = scrollingDirection === 'down'; + const isTransparent = scrollingDirection === 'none'; + + return ( + + ); +} + +function NavItem({ href, title, outlined }: SingleNavItem) { + const { setIsModalOpened } = useNewsletterModalContext(); + + function showNewsletterModal() { + setIsModalOpened(true); + } + + if (outlined) { + return {title}; + } + + return ( + + + {title} + + + ); +} + +const CustomButton = styled(Button)` + padding: 0.75rem 1.5rem; + line-height: 1.8; +`; + +const NavItemList = styled.div` + display: flex; + list-style: none; + + ${media('=desktop')} { + display: none; + } +`; + +const LogoWrapper = styled.a` + display: flex; + margin-right: auto; + text-decoration: none; + + color: rgb(var(--logoColor)); +`; + +const NavItemWrapper = styled.li>` + background-color: ${(p) => (p.outlined ? 'rgb(var(--primary))' : 'transparent')}; + border-radius: 0.5rem; + font-size: 1.3rem; + text-transform: uppercase; + line-height: 2; + + &:hover { + background-color: ${(p) => (p.outlined ? 'rgb(var(--primary), 0.8)' : 'transparent')}; + transition: background-color 0.2s; + } + + a { + display: flex; + color: ${(p) => (p.outlined ? 'rgb(var(--textSecondary))' : 'rgb(var(--text), 0.75)')}; + letter-spacing: 0.025em; + text-decoration: none; + padding: 0.75rem 1.5rem; + font-weight: 700; + } + + &:not(:last-child) { + margin-right: 2rem; + } +`; + +const NavbarContainer = styled.div` + display: flex; + position: sticky; + top: 0; + padding: 1.5rem 0; + width: 100%; + height: 8rem; + z-index: var(--z-navbar); + + background-color: rgb(var(--navbarBackground)); + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 5%); + visibility: ${(p) => (p.hidden ? 'hidden' : 'visible')}; + transform: ${(p) => (p.hidden ? `translateY(-8rem) translateZ(0) scale(1)` : 'translateY(0) translateZ(0) scale(1)')}; + + transition-property: transform, visibility, height, box-shadow, background-color; + transition-duration: 0.15s; + transition-timing-function: ease-in-out; +`; + +const Content = styled(Container)` + display: flex; + justify-content: flex-end; + align-items: center; +`; + +const ColorSwitcherContainer = styled.div` + width: 4rem; + margin: 0 1rem; +`; diff --git a/components/NavigationDrawer.tsx b/components/NavigationDrawer.tsx new file mode 100644 index 0000000..2d8dfbb --- /dev/null +++ b/components/NavigationDrawer.tsx @@ -0,0 +1,124 @@ +import NextLink from 'next/link' +import { useRouter } from 'next/router' +import { PropsWithChildren, useEffect, useRef } from 'react' +import styled from 'styled-components' +import { NavItems } from 'types' +import ClientOnly from './ClientOnly' +import CloseIcon from './CloseIcon' +import OriginalDrawer from './Drawer' + +type NavigationDrawerProps = PropsWithChildren<{ items: NavItems }> + +export default function NavigationDrawer({ children, items }: NavigationDrawerProps) { + return ( + + + + +
+
+ + +
+
+
+
+
+ {children} +
+ ) +} + +function NavItemsList({ items }: NavigationDrawerProps) { + const { close } = OriginalDrawer.useDrawer() + const router = useRouter() + + useEffect(() => { + function handleRouteChangeComplete() { + close() + } + + router.events.on('routeChangeComplete', handleRouteChangeComplete) + return () => router.events.off('routeChangeComplete', handleRouteChangeComplete) + }, [close, router]) + + return ( +
    + {items.map((singleItem, idx) => { + return ( + + {singleItem.title} + + ) + })} +
+ ) +} + +function DrawerCloseButton() { + const ref = useRef(null) + const a11yProps = OriginalDrawer.useA11yCloseButton(ref) + + return +} + +const Wrapper = styled.div` + .my-drawer { + width: 100%; + height: 100%; + z-index: var(--z-drawer); + background: rgb(var(--background)); + transition: margin-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895); + overflow: hidden; + } + + .my-drawer-container { + position: relative; + height: 100%; + margin: auto; + max-width: 70rem; + padding: 0 1.2rem; + } + + .close-icon { + position: absolute; + right: 2rem; + top: 2rem; + } + + .drawer-closed { + margin-left: -100%; + } + + .drawer-opened { + margin-left: 0; + } + + ul { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0; + margin: 0; + list-style: none; + + & > *:not(:last-child) { + margin-bottom: 3rem; + } + } +` + +const NavItem = styled.li` + a { + font-size: 3rem; + text-transform: uppercase; + display: block; + color: currentColor; + text-decoration: none; + border-radius: 0.5rem; + padding: 0.5rem 1rem; + text-align: center; + } +` diff --git a/components/NewsletterModal.tsx b/components/NewsletterModal.tsx new file mode 100644 index 0000000..a1f912e --- /dev/null +++ b/components/NewsletterModal.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react'; +import MailchimpSubscribe, { DefaultFormFields } from 'react-mailchimp-subscribe'; +import styled from 'styled-components'; +import { EnvVars } from 'env'; +import useEscClose from 'hooks/useEscKey'; +import { media } from 'utils/media'; +import Button from './Button'; +import CloseIcon from './CloseIcon'; +import Container from './Container'; +import Input from './Input'; +import MailSentState from './MailSentState'; +import Overlay from './Overlay'; + +export interface NewsletterModalProps { + onClose: () => void; +} + +export default function NewsletterModal({ onClose }: NewsletterModalProps) { + const [email, setEmail] = useState(''); + + useEscClose({ onClose }); + + function onSubmit(event: React.FormEvent, enrollNewsletter: (props: DefaultFormFields) => void) { + event.preventDefault(); + console.log({ email }); + if (email) { + enrollNewsletter({ EMAIL: email }); + } + } + + return ( + { + const hasSignedUp = status === 'success'; + return ( + + + ) => onSubmit(event, subscribe)}> + + + + {hasSignedUp && } + {!hasSignedUp && ( + <> + Are you ready to enroll to the best newsletter ever? + + ) => setEmail(e.target.value)} + placeholder="Enter your email..." + required + /> + + Submit + + + {message && } + + )} + + + + ); + }} + /> + ); +} + +const Card = styled.form` + display: flex; + position: relative; + flex-direction: column; + margin: auto; + padding: 10rem 5rem; + background: rgb(var(--modalBackground)); + border-radius: 0.6rem; + max-width: 70rem; + overflow: hidden; + color: rgb(var(--text)); + + ${media('<=tablet')} { + padding: 7.5rem 2.5rem; + } +`; + +const CloseIconContainer = styled.div` + position: absolute; + right: 2rem; + top: 2rem; + + svg { + cursor: pointer; + width: 2rem; + } +`; + +const Title = styled.div` + font-size: 3.2rem; + font-weight: bold; + line-height: 1.1; + letter-spacing: -0.03em; + text-align: center; + color: rgb(var(--text)); + + ${media('<=tablet')} { + font-size: 2.6rem; + } +`; + +const ErrorMessage = styled.p` + color: rgb(var(--errorColor)); + font-size: 1.5rem; + margin: 1rem 0; + text-align: center; +`; + +const Row = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + margin-top: 3rem; + + ${media('<=tablet')} { + flex-direction: column; + } +`; + +const CustomButton = styled(Button)` + height: 100%; + padding: 1.8rem; + margin-left: 1.5rem; + box-shadow: var(--shadow-lg); + + ${media('<=tablet')} { + width: 100%; + margin-left: 0; + margin-top: 1rem; + } +`; + +const CustomInput = styled(Input)` + width: 60%; + + ${media('<=tablet')} { + width: 100%; + } +`; diff --git a/components/NotFoundIllustration.tsx b/components/NotFoundIllustration.tsx new file mode 100644 index 0000000..f5f4b1d --- /dev/null +++ b/components/NotFoundIllustration.tsx @@ -0,0 +1,465 @@ +export default function NotFoundIllustration() { + return ( + + + + + + + + + + warning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components/OverTitle.tsx b/components/OverTitle.tsx new file mode 100644 index 0000000..63280d7 --- /dev/null +++ b/components/OverTitle.tsx @@ -0,0 +1,29 @@ +import styled from 'styled-components'; +import { media } from 'utils/media'; + +const OverTitle = styled.span` + display: block; + &::before { + position: relative; + bottom: -0.1em; + content: ''; + display: inline-block; + width: 0.9em; + height: 0.9em; + background-color: rgb(var(--primary)); + line-height: 0; + margin-right: 1em; + } + + font-size: 1.3rem; + letter-spacing: 0.02em; + font-weight: bold; + line-height: 0; + text-transform: uppercase; + + ${media('<=desktop')} { + line-height: 1.5; + } +`; + +export default OverTitle; diff --git a/components/Overlay.tsx b/components/Overlay.tsx new file mode 100644 index 0000000..5ba91bb --- /dev/null +++ b/components/Overlay.tsx @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const Overlay = styled.div` + position: fixed; + inset: 0; + background: rgba(var(--secondary), 0.997); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: var(--z-modal); + color: rgb(var(--textSecondary)); +`; + +export default Overlay; diff --git a/components/Page.tsx b/components/Page.tsx new file mode 100644 index 0000000..bbd8755 --- /dev/null +++ b/components/Page.tsx @@ -0,0 +1,70 @@ +import Head from 'next/head'; +import { PropsWithChildren } from 'react'; +import styled from 'styled-components'; +import { EnvVars } from 'env'; +import { media } from 'utils/media'; +import Container from './Container'; +import SectionTitle from './SectionTitle'; + +export interface PageProps { + title: string; + description?: string; +} + +export default function Page({ title, description, children }: PropsWithChildren) { + return ( + <> + + + {title} | {EnvVars.SITE_NAME} + + + + + + + {title} + {description && {description}} + + + + {children} + + + + ); +} + +const Wrapper = styled.div` + background: rgb(var(--background)); +`; + +const HeaderContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + background: rgb(var(--secondary)); + min-height: 40rem; +`; + +const Title = styled(SectionTitle)` + color: rgb(var(--textSecondary)); + margin-bottom: 2rem; +`; + +const Description = styled.div` + font-size: 1.8rem; + color: rgba(var(--textSecondary), 0.8); + text-align: center; + max-width: 60%; + margin: auto; + + ${media('<=tablet')} { + max-width: 100%; + } +`; + +const ChildrenWrapper = styled.div` + margin-top: 10rem; + margin-bottom: 10rem; +`; diff --git a/components/PricingCard.tsx b/components/PricingCard.tsx new file mode 100644 index 0000000..1af4ec7 --- /dev/null +++ b/components/PricingCard.tsx @@ -0,0 +1,98 @@ +import { PropsWithChildren } from 'react'; +import styled from 'styled-components'; +import { media } from 'utils/media'; +import Button from './Button'; +import RichText from './RichText'; + +interface PricingCardProps { + title: string; + description: string; + benefits: string[]; + isOutlined?: boolean; +} + +export default function PricingCard({ title, description, benefits, isOutlined, children }: PropsWithChildren) { + const isAnyBenefitPresent = benefits?.length; + + return ( + + {title} + {description} + + {children} + {isAnyBenefitPresent && ( + +
    + {benefits.map((singleBenefit, idx) => ( +
  • {singleBenefit}
  • + ))} +
+
+ )} +
+ Get started +
+ ); +} + +const Wrapper = styled.div<{ isOutlined?: boolean }>` + display: flex; + flex-direction: column; + padding: 3rem; + background: rgb(var(--cardBackground)); + box-shadow: ${(p) => (p.isOutlined ? 'var(--shadow-lg)' : 'var(--shadow-md)')}; + transform: ${(p) => (p.isOutlined ? 'scale(1.1)' : 'scale(1.0)')}; + text-align: center; + + & > *:not(:first-child) { + margin-top: 1rem; + } + + ${media('<=desktop')} { + box-shadow: var(--shadow-md); + transform: none; + order: ${(p) => (p.isOutlined ? -1 : 0)}; + } +`; + +const Title = styled.h3` + font-size: 4rem; + text-transform: capitalize; +`; + +const Description = styled.p` + font-size: 2.5rem; +`; + +const PriceContainer = styled.div` + margin: auto; + + & > *:not(:first-child) { + margin-top: 2rem; + } +`; + +const Price = styled.div` + display: flex; + justify-content: center; + align-items: flex-end; + font-size: 4rem; + line-height: 1; + font-weight: bold; + + span { + font-size: 2rem; + font-weight: normal; + } +`; + +const CustomRichText = styled(RichText)` + li { + margin: auto; + width: fit-content; + } +`; + +const CustomButton = styled(Button)` + width: 100%; +`; diff --git a/components/Quote.tsx b/components/Quote.tsx new file mode 100644 index 0000000..1358943 --- /dev/null +++ b/components/Quote.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import styled from 'styled-components'; + +interface QuoteProps { + content: string; + author: string; + cite: string; +} + +export default function Quote({ content, author, cite }: QuoteProps) { + return ( + +
{content}
+ — {author} +
+ ); +} + +const Container = styled.figure` + border-left: 1px solid rgb(var(--secondary)); + padding: 3rem; + quotes: ${`"\\201c" "\\201d" "\\2018" "\\2019"`}; + color: rgb(var(--secondary)); + margin-bottom: 3.7rem; + + &::before { + content: open-quote; + font-size: 8em; + line-height: 0.1em; + margin-right: 0.25em; + vertical-align: -0.4em; + opacity: 0.6; + font-family: arial, sans-serif; + } +`; + +const Blockquote = styled.blockquote` + color: rgb(var(--text)); + display: inline; + font-size: 2.2rem; + line-height: 3rem; + font-style: italic; + hanging-punctuation: first; +`; + +const Caption = styled.figcaption` + color: rgb(var(--text)); + display: block; + font-size: 1.6rem; + margin-top: 2.5rem; +`; diff --git a/components/RichText.tsx b/components/RichText.tsx new file mode 100644 index 0000000..bf2b717 --- /dev/null +++ b/components/RichText.tsx @@ -0,0 +1,63 @@ +import styled from 'styled-components'; +import { media } from 'utils/media'; + +const RichText = styled.div` + font-size: 1.8rem; + opacity: 0.8; + line-height: 1.6; + + ol, + ul { + list-style: none; + padding: 0rem; + + li { + padding-left: 2rem; + position: relative; + + & > * { + display: inline-block; + vertical-align: top; + } + + &::before { + position: absolute; + content: 'L'; + left: 0; + top: 0; + text-align: center; + color: rgb(var(--primary)); + font-family: arial; + transform: scaleX(-1) rotate(-35deg); + } + } + } + + table { + border-collapse: collapse; + + table-layout: fixed; + border-spacing: 0; + border-radius: 5px; + border-collapse: separate; + } + th { + background: rgb(var(--textSecondary)); + } + + th, + td { + border: 1px solid rgb(var(--textSecondary)); + padding: 1rem; + } + + tr:nth-child(even) { + background: rgb(var(--textSecondary)); + } + + ${media('<=desktop')} { + font-size: 1.5rem; + } +`; + +export default RichText; diff --git a/components/SectionTitle.tsx b/components/SectionTitle.tsx new file mode 100644 index 0000000..43c6086 --- /dev/null +++ b/components/SectionTitle.tsx @@ -0,0 +1,16 @@ +import styled from 'styled-components'; +import { media } from 'utils/media'; + +const SectionTitle = styled.div` + font-size: 5.2rem; + font-weight: bold; + line-height: 1.1; + letter-spacing: -0.03em; + text-align: center; + + ${media('<=tablet')} { + font-size: 4.6rem; + } +`; + +export default SectionTitle; diff --git a/components/Separator.tsx b/components/Separator.tsx new file mode 100644 index 0000000..ee9a79e --- /dev/null +++ b/components/Separator.tsx @@ -0,0 +1,14 @@ +import styled from 'styled-components'; +import { media } from 'utils/media'; + +const Separator = styled.div` + margin: 12.5rem 0; + border: 1px solid rgba(var(--secondary), 0.025); + height: 0px; + + ${media('<=tablet')} { + margin: 7.5rem 0; + } +`; + +export default Separator; diff --git a/components/Spacer.tsx b/components/Spacer.tsx new file mode 100644 index 0000000..605808f --- /dev/null +++ b/components/Spacer.tsx @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +const Spacer = styled.hr` + width: 100%; + border-color: currentColor; +`; + +export default Spacer; diff --git a/components/ThreeLayersCircle.tsx b/components/ThreeLayersCircle.tsx new file mode 100644 index 0000000..22880f8 --- /dev/null +++ b/components/ThreeLayersCircle.tsx @@ -0,0 +1,52 @@ +import styled from 'styled-components'; +import { media } from 'utils/media'; + +export interface ThreeLayersCircleProps { + baseColor: string; + secondColor: string; +} + +const ThreeLayersCircle = styled.div` + position: relative; + display: inline-block; + opacity: 0.8; + width: 5rem; + height: 5rem; + border-radius: 100rem; + background: rgb(${(p) => p.baseColor}); + z-index: 0; + transition: background 0.2s; + + ${media('<=tablet')} { + width: 4rem; + height: 4rem; + } + + &:after, + &:before { + content: ''; + position: absolute; + width: 3.5rem; + height: 3.5rem; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 100rem; + z-index: -1; + } + + &:after { + width: 4rem; + height: 4rem; + background: rgb(${(p) => p.secondColor}); + z-index: -2; + } + + &:before { + width: 2rem; + height: 2rem; + background: rgb(${(p) => p.baseColor}); + } +`; + +export default ThreeLayersCircle; diff --git a/components/WaveCta.tsx b/components/WaveCta.tsx new file mode 100644 index 0000000..a59ed3a --- /dev/null +++ b/components/WaveCta.tsx @@ -0,0 +1,63 @@ +import NextLink from 'next/link'; +import styled from 'styled-components'; +import Button from 'components/Button'; +import ButtonGroup from 'components/ButtonGroup'; +import Container from 'components/Container'; +import SectionTitle from 'components/SectionTitle'; +import { useNewsletterModalContext } from 'contexts/newsletter-modal.context'; +import { media } from 'utils/media'; + +export default function WaveCta() { + const { setIsModalOpened } = useNewsletterModalContext(); + + return ( + <> + + + + + + Lorem ipsum dolor, sit amet consectetur adipisicing elit. Temporibus delectus? + + + + + Features + + + + + + + ); +} + +const CtaWrapper = styled.div` + background: rgb(var(--secondary)); + margin-top: -1rem; + padding-bottom: 16rem; + + ${media('<=tablet')} { + padding-top: 8rem; + } +`; + +const Title = styled(SectionTitle)` + color: rgb(var(--textSecondary)); + margin-bottom: 4rem; +`; + +const OutlinedButton = styled(Button)` + border: 1px solid rgb(var(--textSecondary)); + color: rgb(var(--textSecondary)); +`; + +const CustomButtonGroup = styled(ButtonGroup)` + justify-content: center; +`; diff --git a/components/YoutubeVideo.tsx b/components/YoutubeVideo.tsx new file mode 100644 index 0000000..eea8e87 --- /dev/null +++ b/components/YoutubeVideo.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import styled from 'styled-components'; + +import playIcon from '../public/play-icon.svg'; + +interface YoutubeVideoProps { + title?: string; + url: string; +} + +export default function YoutubeVideo(props: YoutubeVideoProps) { + const { title, url } = props; + const videoHash = extractVideoHashFromUrl(url); + const srcDoc = ` + + ${title || + Play the video + `; + return ( + + + + ); +} + +function extractVideoHashFromUrl(url: string) { + const videoHashQueryParamKey = 'v'; + const searchParams = new URL(url).search; + return new URLSearchParams(searchParams).getAll(videoHashQueryParamKey); +} + +export const VideoContainer = styled.div` + display: flex; + position: relative; + border-radius: 20px; + overflow: hidden; + -webkit-mask-image: -webkit-radial-gradient(white, black); + + &:before { + display: block; + content: ''; + width: 100%; + padding-top: 56.25%; + } +`; + +const VideoFrame = styled.iframe` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; +`; diff --git a/contexts/newsletter-modal.context.tsx b/contexts/newsletter-modal.context.tsx new file mode 100644 index 0000000..e72108a --- /dev/null +++ b/contexts/newsletter-modal.context.tsx @@ -0,0 +1,31 @@ +import React, { Dispatch, PropsWithChildren, SetStateAction, useContext, useState } from 'react'; + +interface NewsletterModalContextProps { + isModalOpened: boolean; + setIsModalOpened: Dispatch>; +} + +export const NewsletterModalContext = React.createContext(null); + +export function NewsletterModalContextProvider({ children }: PropsWithChildren) { + const [isModalOpened, setIsModalOpened] = useState(false); + + return ( + + {children} + + ); +} + +export function useNewsletterModalContext() { + const context = useContext(NewsletterModalContext); + if (!context) { + throw new Error('useNewsletterModalContext can only be used inside NewsletterModalContextProvider'); + } + return context; +} diff --git a/env.ts b/env.ts new file mode 100644 index 0000000..aa8020d --- /dev/null +++ b/env.ts @@ -0,0 +1,6 @@ +export const EnvVars = { + SITE_NAME: 'My SaaS Startup', + OG_IMAGES_URL: 'https://next-saas-starter-ashy.vercel.app/og-images/', + URL: 'https://next-saas-starter-ashy.vercel.app/', + MAILCHIMP_SUBSCRIBE_URL: 'https://bstefanski.us5.list-manage.com/subscribe/post?u=66b4c22d5c726ae22da1dcb2e&id=679fb0eec9', +}; diff --git a/hooks/useClipboard.ts b/hooks/useClipboard.ts new file mode 100644 index 0000000..5aef63a --- /dev/null +++ b/hooks/useClipboard.ts @@ -0,0 +1,3 @@ +import { useClipboard } from 'use-clipboard-copy'; + +export { useClipboard }; diff --git a/hooks/useEscKey.ts b/hooks/useEscKey.ts new file mode 100644 index 0000000..42a2fb0 --- /dev/null +++ b/hooks/useEscKey.ts @@ -0,0 +1,25 @@ +import { useCallback, useEffect } from 'react'; + +export interface UseEscCloseProps { + onClose: () => void; +} + +export default function useEscClose({ onClose }: UseEscCloseProps) { + const handleUserKeyPress = useCallback( + (event) => { + const { keyCode } = event; + const escapeKeyCode = 27; + if (keyCode === escapeKeyCode) { + onClose(); + } + }, + [onClose], + ); + + useEffect(() => { + window.addEventListener('keydown', handleUserKeyPress); + return () => { + window.removeEventListener('keydown', handleUserKeyPress); + }; + }, [handleUserKeyPress]); +} diff --git a/hooks/useResizeObserver.ts b/hooks/useResizeObserver.ts new file mode 100644 index 0000000..350e03c --- /dev/null +++ b/hooks/useResizeObserver.ts @@ -0,0 +1,3 @@ +import useResizeObserver from 'use-resize-observer'; + +export { useResizeObserver }; diff --git a/hooks/useScrollPosition.ts b/hooks/useScrollPosition.ts new file mode 100644 index 0000000..44d53fd --- /dev/null +++ b/hooks/useScrollPosition.ts @@ -0,0 +1,17 @@ +import { useScrollPosition as originalUseScrollPosition } from '@n8tb1t/use-scroll-position'; + +declare type ElementRef = React.MutableRefObject; + +type Axis = { x: number; y: number }; +export type ScrollPositionEffectProps = { prevPos: Axis; currPos: Axis }; + +export function useScrollPosition( + effect: (props: ScrollPositionEffectProps) => void, + deps?: React.DependencyList | undefined, + element?: ElementRef | undefined, + useWindow?: boolean | undefined, + wait?: number | undefined, + boundingElement?: ElementRef | undefined, +) { + return originalUseScrollPosition(effect, deps, element, useWindow, wait, boundingElement); +} diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..b4bd511 --- /dev/null +++ b/next.config.js @@ -0,0 +1,30 @@ +const CopyPlugin = require('copy-webpack-plugin'); + +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}); + +module.exports = withBundleAnalyzer({ + reactStrictMode: true, + pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], + images: { + domains: ['github.blog'], + deviceSizes: [320, 640, 1080, 1200], + imageSizes: [64, 128], + }, + swcMinify: true, + compiler: { + styledComponents: true, + }, + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + config.module.rules.push({ + test: /\.svg$/, + issuer: { + and: [/\.(js|ts)x?$/], + }, + use: [{ loader: '@svgr/webpack' }, { loader: 'url-loader' }], + }); + + return config; + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..00bbb2e --- /dev/null +++ b/package.json @@ -0,0 +1,72 @@ +{ + "name": "next-saas-starter", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "yarn tinacms server:start -c \"next dev\"", + "build": "yarn tinacms server:start -c \"next build\"", + "start": "yarn tinacms server:start -c \"next start\"", + "lint": "next lint" + }, + "dependencies": { + "@accessible/drawer": "^3.0.2", + "@n8tb1t/use-scroll-position": "^2.0.3", + "@sendgrid/mail": "^7.7.0", + "@svgr/webpack": "^8.0.0", + "css-in-js-media": "^2.0.1", + "date-fns": "^2.29.3", + "gray-matter": "^4.0.3", + "lodash": "^4.17.21", + "next": "12.1.0", + "nextjs-color-mode": "^1.0.5", + "polished": "^4.1.3", + "prism-react-renderer": "^1.3.5", + "react": "17.0.2", + "react-animate-height": "^2.0.23", + "react-dom": "17.0.2", + "react-hook-form": "^7.17.4", + "react-mailchimp-subscribe": "^2.1.3", + "react-schemaorg": "^2.0.0", + "react-share": "^4.4.1", + "reading-time": "^1.5.0", + "schema-dts": "^1.1.2", + "styled-components": "^5.3.5", + "swiper": "9.3.2", + "tinacms": "^1.5.8", + "url-loader": "^4.1.1", + "use-clipboard-copy": "^0.2.0", + "use-resize-observer": "^9.1.0" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.21.3", + "@fec/remark-a11y-emoji": "^3.1.0", + "@next/bundle-analyzer": "^13.3.1", + "@tinacms/cli": "^0.60.0", + "@types/react": "^17.0.20", + "@types/react-mailchimp-subscribe": "^2.1.1", + "@types/styled-components": "^5.1.25", + "@types/swiper": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.59.9", + "all-contributors-cli": "^6.26.0", + "@babel/eslint-parser": "^7.11.0", + "copy-webpack-plugin": "^11.0.0", + "eslint": "^8.0.0", + "eslint-config-next": "^13.2.4", + "eslint-config-prettier": "^8.3.0", + "eslint-config-react-app": "^7.0.0", + "eslint-plugin-flowtype": "^8.0.0", + "eslint-plugin-import": "^2.24.2", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.25.1", + "eslint-plugin-react-hooks": "^4.6.0", + "next-mdx-remote": "^4.4.1", + "remark-breaks": "^3.0.1", + "remark-external-links": "^9.0.1", + "remark-footnotes": "^4.0.1", + "remark-gfm": "^3.0.1", + "remark-sectionize": "^2.0.0", + "remark-slug": "^7.0.1", + "typescript": "5.1.6" + } +} diff --git a/pages/404.tsx b/pages/404.tsx new file mode 100644 index 0000000..250a869 --- /dev/null +++ b/pages/404.tsx @@ -0,0 +1,39 @@ +import styled from 'styled-components'; +import Container from 'components/Container'; +import NotFoundIllustration from 'components/NotFoundIllustration'; + +export default function NotFoundPage() { + return ( + + + + + + 404 + Oh, that's unfortunate! Page not found 😔 + + + ); +} + +const Wrapper = styled.div` + background: rgb(var(--background)); + margin: 10rem 0; + text-align: center; +`; + +const Title = styled.h1` + font-size: 5rem; + margin-top: 5rem; +`; + +const Description = styled.div` + font-size: 3rem; + opacity: 0.8; + margin-top: 2.5rem; +`; + +const ImageContainer = styled.div` + width: 25rem; + margin: auto; +`; diff --git a/pages/_app.tsx b/pages/_app.tsx new file mode 100644 index 0000000..5a4852a --- /dev/null +++ b/pages/_app.tsx @@ -0,0 +1,94 @@ +import 'swiper/css'; +import 'swiper/css/bundle'; +import 'swiper/css/navigation'; +import 'swiper/css/autoplay'; + +import { AppProps } from 'next/dist/shared/lib/router/router'; +import dynamic from 'next/dynamic'; +import Head from 'next/head'; +import { ColorModeScript } from 'nextjs-color-mode'; +import React, { PropsWithChildren } from 'react'; +import { TinaEditProvider } from 'tinacms/dist/edit-state'; + +import Footer from 'components/Footer'; +import { GlobalStyle } from 'components/GlobalStyles'; +import Navbar from 'components/Navbar'; +import NavigationDrawer from 'components/NavigationDrawer'; +import NewsletterModal from 'components/NewsletterModal'; +import WaveCta from 'components/WaveCta'; +import { NewsletterModalContextProvider, useNewsletterModalContext } from 'contexts/newsletter-modal.context'; +import { NavItems } from 'types'; + +const navItems: NavItems = [ + { title: 'Awesome SaaS Features', href: '/features' }, + { title: 'Pricing', href: '/pricing' }, + { title: 'Contact', href: '/contact' }, + { title: 'Sign up', href: '/sign-up', outlined: true }, +]; + +const TinaCMS = dynamic(() => import('tinacms'), { ssr: false }); + +function MyApp({ Component, pageProps }: AppProps) { + return ( + <> + + + + + {/* */} + {/* */} + + + + + + + + + {(livePageProps: any) => } + + } + > + + + +