From daaa789068cebb5fdfcea6197ade6e663be46e0f Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 30 Nov 2022 23:16:07 -0500 Subject: socialify update --- .changeset/README.md | 8 + .changeset/config.json | 11 + .dockerignore | 7 + .env.example | 3 + .eslintrc | 28 + .github/FUNDING.yml | 2 + .github/workflows/build.yml | 33 + .github/workflows/release.yml | 34 + .gitignore | 30 + .husky/pre-commit | 4 + .husky/prepare-commit-msg | 4 + .prettierignore | 7 + .prettierrc.json | 1 + CHANGELOG.md | 15 + CODE_OF_CONDUCT.md | 76 + CONTRIBUTING.md | 76 + Dockerfile | 43 + LICENSE | 21 + README.md | 101 + common/configHelper.ts | 83 + common/font.test.ts | 23 + common/github/repoQuery.ts | 84 + common/helpers.ts | 172 + common/renderCard.ts | 64 + common/renderPNG.tsx | 21 + common/renderSVG.tsx | 26 + common/twemoji.ts | 44 + common/types/configType.ts | 87 + common/types/queryType.ts | 24 + fly.toml | 43 + jest.config.js | 38 + next-env.d.ts | 5 + next.config.js | 33 + package.json | 85 + pages/404.tsx | 13 + pages/[_owner]/[_name].tsx | 7 + pages/_app.tsx | 85 + pages/api/graphql.ts | 49 + pages/api/image.ts | 18 + pages/api/png.ts | 35 + pages/api/stats.svg.ts | 45 + pages/api/stats.ts | 42 + pages/api/svg.ts | 39 + pages/index.tsx | 7 + postcss.config.js | 6 + public/assets/logo192.png | Bin 0 -> 14457 bytes public/assets/logo512.png | Bin 0 -> 51693 bytes public/assets/socialify.png | Bin 0 -> 105490 bytes public/favicon.ico | Bin 0 -> 15406 bytes public/manifest.json | 25 + public/robots.txt | 3 + public/yoga.wasm | Bin 0 -> 58971 bytes src/components/configuration/checkBoxWrapper.tsx | 37 + src/components/configuration/config.tsx | 248 + src/components/configuration/inputWrapper.tsx | 41 + src/components/configuration/selectWrapper.tsx | 44 + src/components/configuration/textAreaWrapper.tsx | 59 + .../error/__snapshots__/error.test.tsx.snap | 46 + src/components/error/error.test.tsx | 16 + src/components/error/error.tsx | 30 + .../footer/__snapshots__/footer.test.tsx.snap | 30 + src/components/footer/footer.test.tsx | 10 + src/components/footer/footer.tsx | 43 + .../header/__snapshots__/header.test.tsx.snap | 81 + src/components/header/header.test.tsx | 14 + src/components/header/header.tsx | 54 + src/components/hooks/use-autofocus.ts | 13 + src/components/mainRenderer.tsx | 59 + src/components/mainWrapper.tsx | 51 + .../preview/__snapshots__/badge.test.tsx.snap | 21 + .../preview/__snapshots__/card.test.tsx.snap | 165 + src/components/preview/badge.test.tsx | 22 + src/components/preview/badge.tsx | 57 + src/components/preview/card.test.tsx | 163 + src/components/preview/card.tsx | 198 + src/components/preview/preview.tsx | 218 + .../repo/__snapshots__/repo.test.tsx.snap | 87 + src/components/repo/repo.test.tsx | 10 + src/components/repo/repo.tsx | 70 + src/components/toaster.tsx | 38 + src/contexts/ConfigContext.ts | 16 + src/typings/hero-patterns.d.ts | 1 + styles/global.css | 3 + tailwind.config.js | 8 + tsconfig.json | 21 + yarn.lock | 7375 ++++++++++++++++++++ 86 files changed, 11059 insertions(+) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .eslintrc create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100755 .husky/prepare-commit-msg create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 common/configHelper.ts create mode 100644 common/font.test.ts create mode 100644 common/github/repoQuery.ts create mode 100644 common/helpers.ts create mode 100644 common/renderCard.ts create mode 100644 common/renderPNG.tsx create mode 100644 common/renderSVG.tsx create mode 100644 common/twemoji.ts create mode 100644 common/types/configType.ts create mode 100644 common/types/queryType.ts create mode 100644 fly.toml create mode 100644 jest.config.js create mode 100644 next-env.d.ts create mode 100644 next.config.js create mode 100644 package.json create mode 100644 pages/404.tsx create mode 100644 pages/[_owner]/[_name].tsx create mode 100644 pages/_app.tsx create mode 100644 pages/api/graphql.ts create mode 100644 pages/api/image.ts create mode 100644 pages/api/png.ts create mode 100644 pages/api/stats.svg.ts create mode 100644 pages/api/stats.ts create mode 100644 pages/api/svg.ts create mode 100644 pages/index.tsx create mode 100644 postcss.config.js create mode 100755 public/assets/logo192.png create mode 100755 public/assets/logo512.png create mode 100644 public/assets/socialify.png create mode 100755 public/favicon.ico create mode 100644 public/manifest.json create mode 100644 public/robots.txt create mode 100755 public/yoga.wasm create mode 100644 src/components/configuration/checkBoxWrapper.tsx create mode 100644 src/components/configuration/config.tsx create mode 100644 src/components/configuration/inputWrapper.tsx create mode 100644 src/components/configuration/selectWrapper.tsx create mode 100644 src/components/configuration/textAreaWrapper.tsx create mode 100644 src/components/error/__snapshots__/error.test.tsx.snap create mode 100644 src/components/error/error.test.tsx create mode 100644 src/components/error/error.tsx create mode 100644 src/components/footer/__snapshots__/footer.test.tsx.snap create mode 100644 src/components/footer/footer.test.tsx create mode 100644 src/components/footer/footer.tsx create mode 100644 src/components/header/__snapshots__/header.test.tsx.snap create mode 100644 src/components/header/header.test.tsx create mode 100644 src/components/header/header.tsx create mode 100644 src/components/hooks/use-autofocus.ts create mode 100644 src/components/mainRenderer.tsx create mode 100644 src/components/mainWrapper.tsx create mode 100644 src/components/preview/__snapshots__/badge.test.tsx.snap create mode 100644 src/components/preview/__snapshots__/card.test.tsx.snap create mode 100644 src/components/preview/badge.test.tsx create mode 100644 src/components/preview/badge.tsx create mode 100644 src/components/preview/card.test.tsx create mode 100644 src/components/preview/card.tsx create mode 100644 src/components/preview/preview.tsx create mode 100644 src/components/repo/__snapshots__/repo.test.tsx.snap create mode 100644 src/components/repo/repo.test.tsx create mode 100644 src/components/repo/repo.tsx create mode 100644 src/components/toaster.tsx create mode 100644 src/contexts/ConfigContext.ts create mode 100644 src/typings/hero-patterns.d.ts create mode 100644 styles/global.css create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..16879c3 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "master", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c550055 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +.git diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6bbd596 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +GITHUB_TOKEN= +PROJECT_URL=http://localhost:3000 +GTM_ID= diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..a524dcc --- /dev/null +++ b/.eslintrc @@ -0,0 +1,28 @@ +{ + "env": { + "browser": true, + "es6": true, + "node": true + }, + "extends": [ + "react-app", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:jest/recommended", + "prettier-standard" + ], + "parserOptions": { + "project": "./tsconfig.json" + }, + "plugins": ["react", "@typescript-eslint", "react-hooks", "prettier"], + "rules": { + "no-use-before-define": "off", + "react/react-in-jsx-scope": "off", + "react/no-unknown-property": [ + 2, + { + "ignore": ["jsx", "global"] + } + ] + } +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f6fca58 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [wei] +liberapay: CryogenicPlanet diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2735c52 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,33 @@ +name: Node.js CI + +on: + push: + pull_request: + types: [assigned, opened, synchronize, reopened] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - name: Cache yarn and next + uses: actions/cache@v2 + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + ${{ github.workspace }}/.next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}- + - run: yarn install + - run: yarn verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7a41eba --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +on: + push: + branches: + - master + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Install Dependencies + run: yarn + + - name: Create Release Pull Request + uses: changesets/action@v1 + with: + commit: 🔖 Bump version + title: 🔖 Bump version + publish: echo "Creating GitHub Release" + createGithubReleases: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..948badf --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local +*.tsbuildinfo + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.env +/.next +.swc + +.idea +.vercel diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..bd82a85 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn verify diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg new file mode 100755 index 0000000..b52f190 --- /dev/null +++ b/.husky/prepare-commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +# gitmoji as a commit hook +exec < /dev/tty +gitmoji --hook $1 $2 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..03d4a0d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +*.graphql +__generated__/ +package.json +build/ +.next/ +.vercel +google-fonts.css diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..c18a05d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +"prettier-config-standard" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4cf82d3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# socialify + +## 2.0.0 + +### Major Changes + +- f1a3f6b: Satori and @vercel/og for image generation + Use vercel edge functions + Upgrade to Tailwind CSS and daisyUI + +## 1.1.0 + +### Minor Changes + +- 76fcfd7: Add changeset diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..97a2fb6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at github@weispot.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fe58afb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at github@weispot.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b629d05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# Install dependencies only when needed +FROM node:16-alpine AS builder +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY . . +RUN yarn install --frozen-lockfile + +# If using npm with a `package-lock.json` comment out above and use below instead +# RUN npm ci + +ENV NEXT_TELEMETRY_DISABLED 1 + +# Add `ARG` instructions below if you need `NEXT_PUBLIC_` variables +# then put the value on your fly.toml +# Example: +# ARG NEXT_PUBLIC_EXAMPLE="value here" + +RUN yarn build + +# If using npm comment out above and use below instead +# RUN npm run build + +# Production image, copy all the files and run next +FROM node:16-alpine AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app ./ + +USER nextjs + +ENV PORT 8080 + +CMD ["yarn", "start"] + +# If using npm comment out above and use below instead +# CMD ["npm", "run", "start"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..db20e9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Wei He + +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..6c660a3 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +[![GitHub Socialify][socialify-image]][socialify-edit-link] + +Want your project to stand out? **Socialify** helps you showcase your project to the world by generating a beautiful project image like the one above! + +It includes a ton of options including custom logo, description, badges, and many fonts and background patterns to choose from. + +Join [![thousands of repositories](https://socialify.git.ci/api/stats.svg)](https://github.com/search?o=desc&q=%22socialify.git.ci%22&s=indexed&type=Code) today! + +## Usage + +Project site: https://socialify.git.ci + +### Social Image as a Service + +Click on the image to use the link anywhere, the image will be programmatically generated with live data. This means the badges will automatically update. + +**Recommended for `README` files or `img` tags.** + +### Image Download + +You can download the image as a `.png`, `.jpeg` or `.webp` and use it anywhere in the world! + +**Recommended for GitHub repository [social preview image](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-your-repositorys-social-media-preview)** and other sites that require image upload. + +### CLI + +You can use cli tool [mheap/github-social-image](https://github.com/mheap/github-social-image) to upload social images to all your repos at once. + +## Examples + +

+ + Session Bot Example + + + + Class Photo Example + + + + Modfy Example + + + + Pull Example + + + + Traefik Example + + + + React Example + +

+ +## Development + +- Create a GitHub token from `Settings > Developer settings > Personal access tokens`, you'll need it in when setting up environemnt variables. +- You'll need the `repo` scope +- Run the following commands to set up the Development server: + + ```shell + # Clone + git clone https://github.com/wei/socialify.git && cd $_ + + # Set environment variables in .env + cp .env.example .env + + yarn install + yarn build + yarn dev + ``` + +## Authors + +- [@CryogenicPlanet](https://github.com/CryogenicPlanet) +- [@wei](https://github.com/wei) + +_Part of [@MLH-Fellowship](https://github.com/MLH-Fellowship) Pod 1.0.6_ + +## License + +- [MIT](https://wei.mit-license.org) + +## SLA + +Socialify is under active development. Design and project domain are subject to change without notice. + +Please subscribe to [#47](https://github.com/wei/socialify/issues/47) if you would like to receive service updates. + +Consider downloading the images or self-hosting should this be a problem. + +[socialify-image]: https://socialify.git.ci/wei/socialify/image?description=1&font=Raleway&issues=1&language=1&pattern=Charlie%20Brown&pulls=1&stargazers=1&theme=Light +[socialify-edit-link]: https://socialify.git.ci/wei/socialify?description=1&font=Raleway&issues=1&language=1&pattern=Charlie%20Brown&pulls=1&stargazers=1&theme=Light + +## Privacy + +Socialify does not collect any personal or identifiable information, we do not use cookies, do not collect emails or anything. + +Socialify uses Google Analytics on web pages to understand overall usage. Generated image links do not have any tracking. diff --git a/common/configHelper.ts b/common/configHelper.ts new file mode 100644 index 0000000..8492584 --- /dev/null +++ b/common/configHelper.ts @@ -0,0 +1,83 @@ +import { RepoQueryResponse } from './github/repoQuery' +import Configuration, { + Font, + OptionalConfigs, + OptionalConfigsKeys, + Pattern, + Theme +} from './types/configType' +import QueryType from './types/queryType' + +type Key = keyof typeof OptionalConfigsKeys + +const DEFAULT_CONFIG: Configuration = { + logo: '', + font: Font.inter, + theme: Theme.light, + pattern: Pattern.plus +} + +const getOptionalConfig = (repository: RepoQueryResponse['repository']) => { + if (repository) { + const languages = repository.languages?.nodes || [] + const language = + languages.length > 0 ? languages[0]?.name || 'unknown' : 'unknown' + const language2 = + languages.length > 1 ? languages[1]?.name || 'unknown' : 'unknown' + const newConfig: OptionalConfigs = { + owner: { state: false, value: repository.owner.login }, + name: { state: true, value: repository.name }, + description: { + state: false, + editable: true, + value: repository.description || '' + }, + language: { state: false, value: language }, + language2: { state: false, value: language2 }, + stargazers: { state: false, value: repository.stargazerCount }, + forks: { state: false, value: repository.forkCount }, + pulls: { state: false, value: repository.pullRequests.totalCount }, + issues: { state: false, value: repository.issues.totalCount } + } + return newConfig + } + return null +} + +const mergeConfig = ( + repository: RepoQueryResponse['repository'], + query: QueryType +): Configuration | null => { + if (!repository) { + return null + } + + const config: Configuration = { + logo: query.logo || DEFAULT_CONFIG.logo, + font: query.font || DEFAULT_CONFIG.font, + pattern: query.pattern || DEFAULT_CONFIG.pattern, + theme: query.theme || DEFAULT_CONFIG.theme + } + const optionalConfig = getOptionalConfig(repository) + + if (optionalConfig) { + Object.assign(config, optionalConfig) + for (const key in query) { + if (key in OptionalConfigsKeys) { + Object.assign(config[key as Key] ?? {}, { + state: query[key as Key] === '1' + }) + if (config[key as Key]?.editable) { + const editableValue = query[`${key}Editable` as keyof typeof query] + if (editableValue) { + Object.assign(config[key as Key] ?? {}, { value: editableValue }) + } + } + } + } + } + + return config +} + +export { DEFAULT_CONFIG, getOptionalConfig, mergeConfig } diff --git a/common/font.test.ts b/common/font.test.ts new file mode 100644 index 0000000..99a568d --- /dev/null +++ b/common/font.test.ts @@ -0,0 +1,23 @@ +/** + * @jest-environment node + */ + +import { SatoriOptions } from 'satori' +import { getFont } from './renderCard' +import { Font } from './types/configType' + +describe('Verify Fonts', () => { + for (const item in Font) { + const fontName = Font[item as keyof typeof Font] + + for (const weight of [200, 400, 500]) { + test(`Check font '${fontName}', ${weight} exists`, async () => { + const { data } = await getFont( + fontName, + weight as SatoriOptions['fonts'][0]['weight'] + ) + expect(data).toBeTruthy() + }) + } + } +}) diff --git a/common/github/repoQuery.ts b/common/github/repoQuery.ts new file mode 100644 index 0000000..aa713e9 --- /dev/null +++ b/common/github/repoQuery.ts @@ -0,0 +1,84 @@ +import { HOST_PREFIX } from '../helpers' + +export const getRepoDetails = async (owner: string, name: string) => { + const body = { + query: ` + query repoQuery($_owner: String!, $_name: String!) { + repository(owner: $_owner, name: $_name) { + forkCount + description + createdAt + name + stargazerCount + issues(states: OPEN) { + totalCount + } + languages(first: 2, orderBy: { field: SIZE, direction: DESC }) { + totalCount + nodes { + name + color + } + } + pullRequests(states: OPEN) { + totalCount + } + releases(last: 1) { + nodes { + tagName + } + } + owner { + login + } + } + } + `, + variables: { + _owner: owner, + _name: name + } + } + + const res = await fetch(`${HOST_PREFIX}/graphql`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify(body) + }) + + const json = await res.json() + return json.data as RepoQueryResponse +} + +export type RepoQueryResponse = { + readonly repository: { + readonly forkCount: number + readonly description: string | null + readonly createdAt: unknown + readonly name: string + readonly stargazerCount: number + readonly issues: { + readonly totalCount: number + } + readonly languages: { + readonly totalCount: number + readonly nodes: ReadonlyArray<{ + readonly name: string + readonly color: string | null + } | null> | null + } | null + readonly pullRequests: { + readonly totalCount: number + } + readonly releases: { + readonly nodes: ReadonlyArray<{ + readonly tagName: string + } | null> | null + } + readonly owner: { + readonly login: string + } + } | null +} diff --git a/common/helpers.ts b/common/helpers.ts new file mode 100644 index 0000000..029df25 --- /dev/null +++ b/common/helpers.ts @@ -0,0 +1,172 @@ +import { Pattern, Theme } from './types/configType' +import { + siGithub, + siC, + siCsharp, + siCplusplus, + siCoffeescript, + siCss3, + siGo, + siApachegroovy, + siHtml5, + siOpenjdk, + siJavascript, + siJupyter, + siPhp, + siPython, + siRuby, + siRust, + siScala, + siSwift, + siTypescript, + siSvelte, + siHaskell, + siKotlin, + siDocker, + siGnubash, + siVuedotjs, + siNginx, + siDart, + siLua, + siDm, + siPerl, + siOcaml, + siClojure, + siPowershell, + siErlang, + siJulia, + siWebassembly, + siPuppet, + siElixir +} from 'simple-icons/icons' +import { + signal, + charlieBrown, + formalInvitation, + plus, + circuitBoard, + overlappingHexagons, + brickWall, + floatingCogs, + diagonalStripes +} from 'hero-patterns' + +const LANGUAGE_ICON_MAPPING: { [key: string]: any } = { + GitHub: siGithub, + C: siC, + 'C#': siCsharp, + 'C++': siCplusplus, + CoffeeScript: siCoffeescript, + CSS: siCss3, + Go: siGo, + Groovy: siApachegroovy, + HTML: siHtml5, + Java: siOpenjdk, + JavaScript: siJavascript, + 'Jupyter Notebook': siJupyter, + PHP: siPhp, + Python: siPython, + Ruby: siRuby, + Rust: siRust, + Scala: siScala, + Swift: siSwift, + TypeScript: siTypescript, + Svelte: siSvelte, + Haskell: siHaskell, + Kotlin: siKotlin, + Dockerfile: siDocker, + Shell: siGnubash, + Vue: siVuedotjs, + Nginx: siNginx, + Dart: siDart, + Lua: siLua, + DM: siDm, + Perl: siPerl, + OCaml: siOcaml, + Clojure: siClojure, + PowerShell: siPowershell, + Erlang: siErlang, + Julia: siJulia, + WebAssembly: siWebassembly, + Puppet: siPuppet, + Elixir: siElixir +} + +const getSimpleIconsImageURI = function (language: string, theme: Theme) { + const icon = LANGUAGE_ICON_MAPPING[language] + if (!icon) return undefined + + const iconColor = theme === Theme.light ? `#${icon.hex}` : `#${icon.hex}` + const iconSvg = icon.svg.replace(' { + const PATTERN_FUNCTIONS_MAPPING: { [key: string]: any } = { + [Pattern.signal]: signal, + [Pattern.charlieBrown]: charlieBrown, + [Pattern.formalInvitation]: formalInvitation, + [Pattern.plus]: plus, + [Pattern.circuitBoard]: circuitBoard, + [Pattern.overlappingHexagons]: overlappingHexagons, + [Pattern.brickWall]: brickWall, + [Pattern.floatingCogs]: floatingCogs, + [Pattern.diagonalStripes]: diagonalStripes, + [Pattern.solid]: null + } + const patternFunction = PATTERN_FUNCTIONS_MAPPING[pattern] + const themedBackgroundColor = theme === Theme.dark ? '#000' : '#fff' + + if (!patternFunction) { + return { + backgroundColor: themedBackgroundColor + } + } + + const darkThemeArgs = ['#eaeaea', 0.2] + const lightThemeArgs = ['#eaeaea', 0.6] + let patternImageUrl = patternFunction.apply( + null, + theme === Theme.dark ? darkThemeArgs : lightThemeArgs + ) + + const width = patternImageUrl.match(/width%3D%22(\d+)%22/)?.[1] + const height = patternImageUrl.match(/height%3D%22(\d+)%22/)?.[1] + + // Satori has issues with quotes around data uris, therefore we are stripping the quotes + patternImageUrl = patternImageUrl + .replace(/^url\('/, 'url(') + .replace(/'\)$/, ')') + + return { + backgroundColor: themedBackgroundColor, + backgroundImage: patternImageUrl, + backgroundSize: `${width}px ${height}px`, + backgroundRepeat: 'repeat' + } +} + +let webpSupport: boolean | undefined +const checkWebpSupport = (): boolean => { + if (webpSupport !== undefined) { + return webpSupport + } + + webpSupport = (() => { + try { + const canvas = document.createElement('canvas') + return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0 + } catch (e) { + return false + } + })() + + return webpSupport +} + +const HOST_PREFIX = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : process.env.PROJECT_URL || '' + +export { getSimpleIconsImageURI, getHeroPattern, checkWebpSupport, HOST_PREFIX } diff --git a/common/renderCard.ts b/common/renderCard.ts new file mode 100644 index 0000000..0e1bb63 --- /dev/null +++ b/common/renderCard.ts @@ -0,0 +1,64 @@ +import { SatoriOptions } from 'satori' +import { Font } from './types/configType' +import QueryType from './types/queryType' +import { mergeConfig } from './configHelper' +import { getRepoDetails } from './github/repoQuery' +import getTwemojiMap from './twemoji' + +export async function getFont( + font: Font, + weight: SatoriOptions['fonts'][0]['weight'] +): Promise { + const fontSlug = font.replace(/\s/g, '-').toLowerCase() + const cdnUrl = `https://cdn.jsdelivr.net/npm/@fontsource/${fontSlug}/files/${fontSlug}-all-${weight}-normal.woff` + + return { + name: font, + data: await fetch(cdnUrl).then((response) => { + if (response.ok) { + return response.arrayBuffer() + } + throw new Error('Failed to fetch font') + }), + weight, + style: 'normal' + } +} + +export function getFonts(font: Font) { + return Promise.all([ + getFont(Font.jost, 400), + getFont(font, 200), + getFont(font, 400), + getFont(font, 500) + ]) +} + +export async function getEmojiSVG(code: string) { + return ( + await fetch(`https://twemoji.maxcdn.com/v/13.1.0/svg/${code}.svg`) + ).text() +} + +export async function getGraphemeImages(description: string = '') { + const emojiCodes = getTwemojiMap(description) + const emojis = await Promise.all(Object.values(emojiCodes).map(getEmojiSVG)) + const graphemeImages = Object.fromEntries( + Object.entries(emojiCodes).map(([key], index) => [ + key, + `data:image/svg+xml;base64,` + btoa(emojis[index]) + ]) + ) + + return graphemeImages +} + +export async function getCardConfig(query: QueryType) { + const { repository } = await getRepoDetails(query._owner, query._name) + + const config = mergeConfig(repository, query) + + if (!config) throw Error('Configuration failed to generate') + + return config +} diff --git a/common/renderPNG.tsx b/common/renderPNG.tsx new file mode 100644 index 0000000..223dc4c --- /dev/null +++ b/common/renderPNG.tsx @@ -0,0 +1,21 @@ +import { ImageResponse } from '@vercel/og' +import Card from '../src/components/preview/card' +import { getCardConfig, getFonts } from './renderCard' +import QueryType from './types/queryType' + +const renderCardPNG = async ( + query: QueryType, + opts: { headers?: Record } = {} +) => { + const config = await getCardConfig(query) + + return new ImageResponse(, { + width: 1280, + height: 640, + fonts: await getFonts(config.font), + emoji: 'twemoji', + ...opts + }) +} + +export default renderCardPNG diff --git a/common/renderSVG.tsx b/common/renderSVG.tsx new file mode 100644 index 0000000..d3ec6fe --- /dev/null +++ b/common/renderSVG.tsx @@ -0,0 +1,26 @@ +// @ts-ignore +import satori, { init as initSatori } from 'satori/wasm' +// @ts-ignore +import initYoga from 'yoga-wasm-web' +// @ts-ignore +import yogaWasm from '../public/yoga.wasm?module' + +import Card from '../src/components/preview/card' +import { getCardConfig, getFonts, getGraphemeImages } from './renderCard' +import QueryType from './types/queryType' + +const renderCardSVG = async (query: QueryType) => { + const yoga = await initYoga(yogaWasm) + initSatori(yoga) + + const config = await getCardConfig(query) + + return satori(, { + width: 1280, + height: 640, + fonts: await getFonts(config.font), + graphemeImages: await getGraphemeImages(config.description?.value) + }) +} + +export default renderCardSVG diff --git a/common/twemoji.ts b/common/twemoji.ts new file mode 100644 index 0000000..bb433fa --- /dev/null +++ b/common/twemoji.ts @@ -0,0 +1,44 @@ +/** + * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js. + */ + +/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */ + +const re = + /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91])|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udeeb\udeec\udef4-\udefc\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78\udd7a-\uddb4\uddb7\uddba\uddbc-\uddcb\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7a\ude80-\ude86\ude90-\udea8\udeb0-\udeb6\udec0-\udec2\uded0-\uded6]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g + +const U200D = String.fromCharCode(8205) +const UFE0Fg = /\uFE0F/g + +function grabTheRightIcon(emoji: string) { + return toCodePoint( + emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji + ) +} + +function toCodePoint(unicodeSurrogates: string) { + const r = [] + let c = 0 + let p = 0 + let i = 0 + while (i < unicodeSurrogates.length) { + c = unicodeSurrogates.charCodeAt(i++) + if (p) { + r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16)) + p = 0 + } else if (c >= 55296 && c <= 56319) { + p = c + } else { + r.push(c.toString(16)) + } + } + return r.join('-') +} + +export default function getTwemojiMap(content: string) { + const codes: Record = {} + for (const match of Array.from(content.matchAll(re))) { + codes[match[0]] = grabTheRightIcon(match[0]) + } + return codes +} diff --git a/common/types/configType.ts b/common/types/configType.ts new file mode 100644 index 0000000..4cf3c96 --- /dev/null +++ b/common/types/configType.ts @@ -0,0 +1,87 @@ +/* eslint-disable no-unused-vars */ +enum Theme { + light = 'Light', + dark = 'Dark' +} + +enum Pattern { + signal = 'Signal', + charlieBrown = 'Charlie Brown', + formalInvitation = 'Formal Invitation', + plus = 'Plus', + circuitBoard = 'Circuit Board', + overlappingHexagons = 'Overlapping Hexagons', + brickWall = 'Brick Wall', + floatingCogs = 'Floating Cogs', + diagonalStripes = 'Diagonal Stripes', + solid = 'Solid' +} + +enum Font { + inter = 'Inter', + bitter = 'Bitter', + raleway = 'Raleway', + rokkitt = 'Rokkitt', + sourceCodePro = 'Source Code Pro', + koHo = 'KoHo', + jost = 'Jost' +} + +export type RequiredConfigs = { + logo: string + + font: Font + theme: Theme + pattern: Pattern +} + +const OptionalConfigKeyStrings = { + owner: true, + name: true, + description: true, + language: true, + language2: true +} + +const OptionalConfigKeyNumbers = { + stargazers: true, + forks: true, + issues: true, + pulls: true +} + +export const RequiredConfigsKeys = { + logo: true, + font: true, + theme: true, + pattern: true +} + +export const OptionalConfigsKeys = { + ...OptionalConfigKeyStrings, + ...OptionalConfigKeyNumbers +} + +type OptionalConfigStringElement = { + [name in keyof typeof OptionalConfigKeyStrings]?: { + state: boolean + value: string + editable?: boolean + } +} +type OptionalConfigNumberElement = { + [name in keyof typeof OptionalConfigKeyNumbers]?: { + state: boolean + value: number + editable?: boolean + } +} + +export type OptionalConfigs = OptionalConfigStringElement & + OptionalConfigNumberElement + +type Configuration = RequiredConfigs & OptionalConfigs + +export default Configuration + +export { Theme, Pattern, Font } diff --git a/common/types/queryType.ts b/common/types/queryType.ts new file mode 100644 index 0000000..42847dc --- /dev/null +++ b/common/types/queryType.ts @@ -0,0 +1,24 @@ +import { Font, Pattern, Theme } from './configType' + +type QueryType = { + font: Font + theme: Theme + pattern: Pattern + + language: string + language2: string + stargazers: string + forks: string + issues: string + pulls: string + description: string + descriptionEditable: string + owner: string + name: string + logo: string + + _owner: string + _name: string +} + +export default QueryType diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..9ae39a6 --- /dev/null +++ b/fly.toml @@ -0,0 +1,43 @@ +# fly.toml file generated for socialify on 2022-11-25T10:10:25-05:00 + +app = "socialify" +kill_signal = "SIGINT" +kill_timeout = 5 +processes = [] + +[build] + [build.args] + NEXT_PUBLIC_EXAMPLE = "Value goes here" + +[env] + PORT = "8080" + +[experimental] + allowed_public_ports = [] + auto_rollback = true + +[[services]] + http_checks = [] + internal_port = 8080 + processes = ["app"] + protocol = "tcp" + script_checks = [] + [services.concurrency] + hard_limit = 25 + soft_limit = 20 + type = "connections" + + [[services.ports]] + force_https = true + handlers = ["http"] + port = 80 + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 + + [[services.tcp_checks]] + grace_period = "1s" + interval = "15s" + restart_limit = 0 + timeout = "2s" diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..0a2bf78 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,38 @@ +// jest.config.js +const nextJest = require('next/jest') + +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.js and .env files in your test environment + dir: './' +}) + +// Add any custom config to be passed to Jest +/** @type {import('jest').Config} */ +const customJestConfig = { + // Add more setup options before each test is run + // setupFilesAfterEnv: ['/jest.setup.js'], + // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work + moduleDirectories: ['node_modules', '/'], + modulePathIgnorePatterns: ['/.vercel/'], + testEnvironment: 'jest-environment-jsdom', + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], + testMatch: [ + '/**/__tests__/**/*.{js,jsx,ts,tsx}', + '/**/*.{spec,test}.{js,jsx,ts,tsx}' + ], + moduleNameMapper: { + '\\.(css|less)$': 'identity-obj-proxy' + }, + transformIgnorePatterns: [ + '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$', + '^.+\\.module\\.(css|sass|scss)$' + ] + // "transform": { + // "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest", + // "^.+\\.css$": "/config/jest/cssTransform.js", + // "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "/config/jest/fileTransform.js" + // }, +} + +// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async +module.exports = createJestConfig(customJestConfig) 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..5b04182 --- /dev/null +++ b/next.config.js @@ -0,0 +1,33 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + eslint: { + ignoreDuringBuilds: true + }, + async rewrites() { + return [ + { + source: '/:_owner/:_name/image', + destination: '/api/image' + }, + { + source: '/:_owner/:_name/svg', + destination: '/api/svg' + }, + { + source: '/:_owner/:_name/png', + destination: '/api/png' + }, + // Kept for legacy support + { + source: '/:_owner/:_name/jpg', + destination: '/api/png' + }, + { + source: '/graphql', + destination: '/api/graphql' + } + ] + } +} + +module.exports = nextConfig diff --git a/package.json b/package.json new file mode 100644 index 0000000..5058aff --- /dev/null +++ b/package.json @@ -0,0 +1,85 @@ +{ + "name": "socialify", + "version": "2.0.0", + "description": "Socialify your project. Share with the world!", + "author": "@CryogenicPlanet, @wei", + "license": "MIT", + "repository": "https://github.com/wei/socialify.git", + "scripts": { + "dev": "next dev", + "debug": "NODE_OPTIONS='--inspect' next", + "build": "next build", + "test": "jest", + "test:watch": "jest --watch", + "start": "next start", + "lint": "eslint . --ext .ts,.tsx", + "lint:fix": "yarn lint --fix", + "prettier": "prettier --check .", + "prettier:fix": "prettier --write .", + "verify": "yarn prettier && yarn lint && yarn tsc && yarn test && yarn build", + "download-font": "./fonts/download-font.sh", + "prepare": "is-ci || husky install" + }, + "dependencies": { + "@changesets/cli": "^2.25.2", + "@vercel/analytics": "^0.1.5", + "@vercel/og": "^0.0.20", + "autoprefixer": "^10.4.13", + "badgen": "^3.2.2", + "clsx": "^1.2.1", + "copee": "^1.0.6", + "cross-fetch": "^3.1.5", + "daisyui": "^2.41.0", + "hero-patterns": "^2.1.0", + "is-ci": "^3.0.1", + "next": "^13.0.4", + "postcss": "^8.4.19", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", + "react-hot-toast": "^2.4.0", + "react-icons": "^4.6.0", + "satori": "^0.0.44", + "simple-icons": "^7.19.0", + "styled-jsx": "^5.1.0", + "tailwindcss": "^3.2.4", + "typescript": "~4.9.3", + "use-debounce": "^8.0.4", + "yoga-wasm-web": "0.1.2" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "eslint": "^8.28.0", + "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier-standard": "^4.0.1", + "eslint-config-react-app": "^7.0.1", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-jest": "^27.1.5", + "eslint-plugin-n": "^15.5.1", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.31.11", + "eslint-plugin-react-hooks": "^4.6.0", + "graphql": "^16.6.0", + "graphql-compiler": "^1.7.0", + "husky": "^8.0.2", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.3.1", + "jest-environment-jsdom": "^29.3.1", + "prettier": "^2.7.1", + "prettier-config-standard": "^5.0.0" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/pages/404.tsx b/pages/404.tsx new file mode 100644 index 0000000..f1ccaab --- /dev/null +++ b/pages/404.tsx @@ -0,0 +1,13 @@ +import Error from '../src/components/error/error' + +const HomePage = () => { + return ( + + ) +} + +export default HomePage diff --git a/pages/[_owner]/[_name].tsx b/pages/[_owner]/[_name].tsx new file mode 100644 index 0000000..6ea6e06 --- /dev/null +++ b/pages/[_owner]/[_name].tsx @@ -0,0 +1,7 @@ +import MainRenderer from '../../src/components/mainRenderer' + +const RepoPage = () => { + return +} + +export default RepoPage diff --git a/pages/_app.tsx b/pages/_app.tsx new file mode 100644 index 0000000..30997d3 --- /dev/null +++ b/pages/_app.tsx @@ -0,0 +1,85 @@ +import App from 'next/app' +import Head from 'next/head' +import Script from 'next/script' +import { Toaster } from 'react-hot-toast' +import { Analytics } from '@vercel/analytics/react' + +import { HOST_PREFIX } from '../common/helpers' + +import '../styles/global.css' + +import HeaderElement from '../src/components/header/header' +import FooterElement from '../src/components/footer/footer' + +const GoogleTagManager = () => { + if (process.env.GTM_ID) { + return ( + <> + + + + ) + } + + return ( + + ) +} + +export default class MyApp extends App { + public render() { + const { Component, pageProps } = this.props + + return ( + <> + + + + + + + + + + + Socialify - That Computer Scientist Fork + {GoogleTagManager()} + +
+ +
+ +
+ + + +
+ + ) + } +} diff --git a/pages/api/graphql.ts b/pages/api/graphql.ts new file mode 100644 index 0000000..92c3eff --- /dev/null +++ b/pages/api/graphql.ts @@ -0,0 +1,49 @@ +import type { NextRequest } from 'next/server' + +const API_ENDPOINT = 'https://api.github.com/graphql' + +const graphQLEndpoint = async (req: NextRequest) => { + if (req.method !== 'POST') { + return new Response('Method Not Allowed', { + status: 405, + headers: { + 'cache-control': 'max-age=0, public' + } + }) + } + + const response = await fetch(API_ENDPOINT, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `bearer ${process.env.GITHUB_TOKEN}`, + 'content-type': 'application/json' + }, + body: req.body + }) + + if (!response.ok) { + return new Response(await response.text(), { + status: response.status, + headers: { + 'cache-control': 'public, max-age=0' + } + }) + } + + const text = await response.text() + return new Response(text, { + status: 200, + headers: { + 'content-type': 'application/json', + 'cache-control': + 'public, immutable, no-transform, max-age=60, s-maxage=600' + } + }) +} + +export const config = { + runtime: 'experimental-edge' +} + +export default graphQLEndpoint diff --git a/pages/api/image.ts b/pages/api/image.ts new file mode 100644 index 0000000..122599e --- /dev/null +++ b/pages/api/image.ts @@ -0,0 +1,18 @@ +import { isBot } from 'next/dist/server/web/spec-extension/user-agent' +import type { NextRequest } from 'next/server' +import pngEndpoint from './png' +import svgEndpoint from './svg' + +const imageEndpoint = async (req: NextRequest) => { + if (isBot(req.headers.get('user-agent') ?? '')) { + return pngEndpoint(req) + } else { + return svgEndpoint(req) + } +} + +export const config = { + runtime: 'experimental-edge' +} + +export default imageEndpoint diff --git a/pages/api/png.ts b/pages/api/png.ts new file mode 100644 index 0000000..dc9883b --- /dev/null +++ b/pages/api/png.ts @@ -0,0 +1,35 @@ +import type { NextRequest } from 'next/server' + +import QueryType from '../../common/types/queryType' +import renderCardPNG from '../../common/renderPNG' + +const pngEndpoint = async (req: NextRequest) => { + const { searchParams } = new URL(req.url) + const query = Object.fromEntries(searchParams) as QueryType + + try { + return renderCardPNG(query, { + headers: { + 'cache-control': `public, immutable, no-transform, max-age=0, s-maxage=${ + searchParams.has('cache') ? searchParams.get('cache') : 3600 + }` + } + }) + } catch (ex) { + console.error(ex) + + return new Response(JSON.stringify({ error: ex }), { + status: 400, + headers: { + 'content-type': 'application/json', + 'cache-control': 'public, max-age=0' + } + }) + } +} + +export const config = { + runtime: 'experimental-edge' +} + +export default pngEndpoint diff --git a/pages/api/stats.svg.ts b/pages/api/stats.svg.ts new file mode 100644 index 0000000..df95a89 --- /dev/null +++ b/pages/api/stats.svg.ts @@ -0,0 +1,45 @@ +import type { NextRequest } from 'next/server' +import { badgen } from 'badgen' +import statsEndpoint from './stats' + +const statsSvgEndpoint = async (req: NextRequest) => { + let totalCount = 0 + + try { + const apiResponse = await (await statsEndpoint(req)).json() + if (apiResponse.total_count) { + totalCount = apiResponse.total_count + } + } catch (ex) { + console.error(ex) + } + + const svg = totalCount + ? badgen({ + subject: '', + status: `${totalCount} repositories`, + color: 'black', + style: 'flat' + }) + : badgen({ + subject: '', + status: `thousands of repositories`, + color: 'black', + style: 'flat' + }) + + return new Response(svg, { + status: 200, + headers: { + 'content-type': 'image/svg+xml', + 'cache-control': + 'public, immutable, no-transform, max-age=60, s-maxage=86400' + } + }) +} + +export const config = { + runtime: 'experimental-edge' +} + +export default statsSvgEndpoint diff --git a/pages/api/stats.ts b/pages/api/stats.ts new file mode 100644 index 0000000..d557939 --- /dev/null +++ b/pages/api/stats.ts @@ -0,0 +1,42 @@ +import type { NextRequest } from 'next/server' + +const statsEndpoint = async (req: NextRequest) => { + const response = await fetch( + `https://api.github.com/search/code?per_page=1&q=${encodeURIComponent( + 'socialify.git.ci' + )}`, + { + method: 'GET', + headers: { + Accept: 'application/vnd.github.v3+json', + Authorization: `bearer ${process.env.GITHUB_TOKEN}`, + 'content-type': 'application/json' + } + } + ) + + if (!response.ok) { + return new Response(await response.text(), { + status: response.status, + headers: { + 'cache-control': 'public, max-age=0' + } + }) + } + + const json = await response.json() + return new Response(JSON.stringify({ total_count: json.total_count }), { + status: 200, + headers: { + 'content-type': 'application/json', + 'cache-control': + 'public, immutable, no-transform, max-age=60, s-maxage=86400' + } + }) +} + +export const config = { + runtime: 'experimental-edge' +} + +export default statsEndpoint diff --git a/pages/api/svg.ts b/pages/api/svg.ts new file mode 100644 index 0000000..a803e9b --- /dev/null +++ b/pages/api/svg.ts @@ -0,0 +1,39 @@ +import type { NextRequest } from 'next/server' + +import QueryType from '../../common/types/queryType' +import renderCardSVG from '../../common/renderSVG' + +const svgEndpoint = async (req: NextRequest) => { + const { searchParams } = new URL(req.url) + const query = Object.fromEntries(searchParams) as QueryType + + try { + const svg = await renderCardSVG(query) + + return new Response(svg, { + status: 200, + headers: { + 'content-type': 'image/svg+xml', + 'cache-control': `public, immutable, no-transform, max-age=0, s-maxage=${ + searchParams.has('cache') ? searchParams.get('cache') : 3600 + }` + } + }) + } catch (ex) { + console.error(ex) + + return new Response(JSON.stringify({ error: ex }), { + status: 400, + headers: { + 'content-type': 'application/json', + 'cache-control': 'public, max-age=0' + } + }) + } +} + +export const config = { + runtime: 'experimental-edge' +} + +export default svgEndpoint diff --git a/pages/index.tsx b/pages/index.tsx new file mode 100644 index 0000000..dce81a1 --- /dev/null +++ b/pages/index.tsx @@ -0,0 +1,7 @@ +import Repo from '../src/components/repo/repo' + +const HomePage = () => { + return +} + +export default HomePage diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..85f717c --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +} diff --git a/public/assets/logo192.png b/public/assets/logo192.png new file mode 100755 index 0000000..46e098a Binary files /dev/null and b/public/assets/logo192.png differ diff --git a/public/assets/logo512.png b/public/assets/logo512.png new file mode 100755 index 0000000..1350714 Binary files /dev/null and b/public/assets/logo512.png differ diff --git a/public/assets/socialify.png b/public/assets/socialify.png new file mode 100644 index 0000000..50be804 Binary files /dev/null and b/public/assets/socialify.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100755 index 0000000..a5ab3c4 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..7007a0b --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Socialify [That Computer Scientist Fork]", + "name": "Socialify your project. Share with the world!", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "assets/logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "assets/logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/public/yoga.wasm b/public/yoga.wasm new file mode 100755 index 0000000..819bba1 Binary files /dev/null and b/public/yoga.wasm differ diff --git a/src/components/configuration/checkBoxWrapper.tsx b/src/components/configuration/checkBoxWrapper.tsx new file mode 100644 index 0000000..e9dd1eb --- /dev/null +++ b/src/components/configuration/checkBoxWrapper.tsx @@ -0,0 +1,37 @@ +import ConfigType from '../../../common/types/configType' + +type CheckBoxProps = { + title: string + keyName: keyof ConfigType + checked?: boolean + disabled?: boolean + + handleChange: (value: any, key: keyof ConfigType) => void +} + +const CheckBoxWrapper = ({ + title, + keyName, + checked, + disabled, + handleChange +}: CheckBoxProps) => { + return ( +
+ +
+ ) +} + +export default CheckBoxWrapper diff --git a/src/components/configuration/config.tsx b/src/components/configuration/config.tsx new file mode 100644 index 0000000..9331f87 --- /dev/null +++ b/src/components/configuration/config.tsx @@ -0,0 +1,248 @@ +import React, { useContext, useEffect } from 'react' +import { useRouter } from 'next/router' + +import { RepoQueryResponse } from '../../../common/github/repoQuery' +import ConfigContext from '../../contexts/ConfigContext' + +import ConfigType, { + Theme, + Pattern, + Font, + RequiredConfigsKeys +} from '../../../common/types/configType' + +import { getOptionalConfig } from '../../../common/configHelper' + +import SelectWrapper from './selectWrapper' +import CheckBoxWrapper from './checkBoxWrapper' +import InputWrapper from './inputWrapper' +import TextAreaWrapper from './textAreaWrapper' + +type ConfigProp = { + repository: RepoQueryResponse['repository'] +} + +const Config = ({ repository }: ConfigProp) => { + const router = useRouter() + + const { config, setConfig } = useContext(ConfigContext) + + const handleChanges = (changes: { value: any; key: keyof ConfigType }[]) => { + let newConfig: ConfigType = { ...config } + const urlParams = router.query + // Remove extraneous params from route + delete urlParams._owner + delete urlParams._name + changes.forEach(({ value, key }) => { + const currentValue = newConfig[key] ? newConfig[key] : {} + if (value.required === true) { + newConfig = { ...newConfig, [key]: value.val } + } else { + newConfig = { ...newConfig, [key]: { ...currentValue, ...value } } + } + + if (value && value.state === true && value.editable) { + urlParams[key] = '1' + urlParams[`${key}Editable`] = value.value + } else if (value && value.state === true) { + urlParams[key] = '1' + } else if (value && value.required === true) { + urlParams[key] = value.val + } else { + urlParams[key] = '0' + } + + if (!urlParams[key] || urlParams[key] === '0') { + delete urlParams[key] + if (`${key}Editable` in urlParams) { + delete urlParams[`${key}Editable`] + } + } + }) + + router.push( + `${window.location.pathname}?${Object.entries(urlParams) + .sort() + .map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`) + .join('&')}`, + undefined, + { shallow: true } + ) + } + + const handleChange = (value: any, key: keyof ConfigType) => { + handleChanges([{ value, key }]) + } + + useEffect(() => { + const handleRouteChange = (asPath: string) => { + if (repository) { + const newConfig = getOptionalConfig(repository) + if (newConfig) { + const params = new URLSearchParams(asPath.split('?')[1]) + + Array.from(params.keys()).forEach((stringKey) => { + const key = stringKey as keyof ConfigType + if (key in newConfig) { + const query = params.get(key) + const currentConfig = newConfig[key as keyof typeof newConfig] + const newChange = { + state: query === '1' + } + if (currentConfig?.editable) { + const editableValue = params.get(`${key}Editable`) + if (editableValue != null) { + Object.assign(newChange, { + value: editableValue + }) + } + } + + Object.assign( + newConfig[key as keyof typeof newConfig] ?? {}, + newChange + ) + } else if (key in RequiredConfigsKeys) { + const query = params.get(key) + if (query != null) { + const newChange = { + [key]: query + } + + Object.assign(newConfig, newChange) + } + } + }) + setConfig({ ...config, ...newConfig }) + } + } + } + + router.events.on('routeChangeComplete', handleRouteChange) + handleRouteChange(router.asPath) + + return () => { + router.events.off('routeChangeComplete', handleRouteChange) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + if (!repository) { + return null + } + + return ( +
+
+ ({ + key, + label: (Theme as any)[key] + }))} + value={config.theme} + handleChange={handleChange} + /> + ({ + key, + label: (Font as any)[key] + }))} + value={config.font} + handleChange={handleChange} + /> + ({ + key, + label: (Pattern as any)[key] + }))} + value={config.pattern} + handleChange={handleChange} + /> + + +
+ + + + + + + + + +
+ + {config.description?.state && ( + + )} +
+
+ ) +} + +export default Config diff --git a/src/components/configuration/inputWrapper.tsx b/src/components/configuration/inputWrapper.tsx new file mode 100644 index 0000000..0d3fd95 --- /dev/null +++ b/src/components/configuration/inputWrapper.tsx @@ -0,0 +1,41 @@ +import ConfigType from '../../../common/types/configType' + +type InputProps = { + title: string + alt?: string + keyName: keyof ConfigType + value: string + placeholder: string + disabled?: boolean + handleChange: (value: any, key: keyof ConfigType) => void +} + +const InputWrapper = ({ + title, + alt, + keyName, + value, + placeholder, + disabled, + handleChange +}: InputProps) => { + return ( +
+ + { + handleChange({ val: e.target.value, required: true }, keyName) + }} + /> +
+ ) +} +export default InputWrapper diff --git a/src/components/configuration/selectWrapper.tsx b/src/components/configuration/selectWrapper.tsx new file mode 100644 index 0000000..81c3709 --- /dev/null +++ b/src/components/configuration/selectWrapper.tsx @@ -0,0 +1,44 @@ +import ConfigType from '../../../common/types/configType' + +type SelectWrapperProps = { + title: string + alt?: string + keyName: keyof ConfigType + map: { key: string; label: any }[] + value: string + handleChange: (value: any, key: keyof ConfigType) => void +} + +const SelectWrapper = ({ + title, + alt, + keyName, + map, + value, + handleChange +}: SelectWrapperProps) => { + return ( +
+ + +
+ ) +} + +export default SelectWrapper diff --git a/src/components/configuration/textAreaWrapper.tsx b/src/components/configuration/textAreaWrapper.tsx new file mode 100644 index 0000000..6a30f42 --- /dev/null +++ b/src/components/configuration/textAreaWrapper.tsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react' + +import { useDebouncedCallback } from 'use-debounce' + +import ConfigType from '../../../common/types/configType' + +type TextAreaProps = { + title?: string + alt?: string + value: string + placeholder?: string + keyName: keyof ConfigType + handleChange: (value: any, key: keyof ConfigType) => void + disabled?: boolean +} + +const TextAreaWrapper = ({ + title, + alt, + keyName, + value, + placeholder, + handleChange, + disabled +}: TextAreaProps) => { + const [internalValue, setInternalValue] = useState(value) + + const debounced = useDebouncedCallback((value) => { + handleChange({ value, editable: true, state: true }, keyName) + }, 500) + + const processChange = (e: React.ChangeEvent) => { + setInternalValue(e.target.value) + debounced(e.target.value) + } + + useEffect(() => { + setInternalValue(value) + }, [value]) + + return ( +
+ {title && ( + + )} +