From 7bd6fdc95ea755a1aa6e311d611c348ff7e7ddcd Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Tue, 12 Mar 2024 20:58:59 +0300 Subject: [PATCH 1/9] Setup outline VPN --- .gitignore | 3 + Dockerfile.vpn-manager | 69 + apps/vpn-manager/.gitignore | 39 + apps/vpn-manager/README.md | 36 + apps/vpn-manager/next.config.mjs | 38 + apps/vpn-manager/package.json | 45 + apps/vpn-manager/public/next.svg | 1 + apps/vpn-manager/public/vercel.svg | 1 + apps/vpn-manager/sentry.client.config.ts | 16 + apps/vpn-manager/sentry.edge.config.ts | 8 + apps/vpn-manager/sentry.server.config.ts | 8 + ...pe=github, Size=24, Color=CurrentColor.svg | 10 + .../Type=x, Size=24, Color=CurrentColor.svg | 4 + .../src/assets/icons/menu-icon.svg | 5 + .../DesktopNavBar/DesktopNavBar.tsx | 57 + .../src/components/DesktopNavBar/index.ts | 3 + .../components/MobileNavBar/MobileNavBar.tsx | 139 ++ .../src/components/MobileNavBar/index.ts | 3 + .../src/components/NavBar/NavBar.tsx | 46 + .../src/components/NavBar/index.ts | 3 + .../NavBarNavList/NavBarNavList.tsx | 103 ++ .../src/components/NavBarNavList/index.ts | 3 + .../components/NavListItem/NavListItem.tsx | 19 + .../src/components/NavListItem/index.ts | 3 + .../NextImageButton/NextImageButton.tsx | 40 + .../src/components/NextImageButton/index.ts | 3 + apps/vpn-manager/src/components/Page/Page.tsx | 39 + apps/vpn-manager/src/components/Page/index.ts | 3 + apps/vpn-manager/src/favicon.ico | Bin 0 -> 15086 bytes apps/vpn-manager/src/globals.css | 107 ++ apps/vpn-manager/src/layout.tsx | 27 + apps/vpn-manager/src/lib/data/spreadsheet.ts | 59 + apps/vpn-manager/src/lib/processNewHires.ts | 17 + apps/vpn-manager/src/page.module.css | 232 ++++ apps/vpn-manager/src/pages/_app.tsx | 39 + apps/vpn-manager/src/pages/_document.tsx | 120 ++ apps/vpn-manager/src/pages/index.tsx | 36 + apps/vpn-manager/src/theme/fonts.css | 136 ++ apps/vpn-manager/src/theme/index.ts | 552 ++++++++ apps/vpn-manager/src/types.d.ts | 41 + .../src/utils/createEmotionCache.ts | 20 + apps/vpn-manager/src/utils/index.ts | 4 + apps/vpn-manager/src/utils/wordToCamelCase.ts | 12 + apps/vpn-manager/tsconfig.json | 28 + apps/vpn-manager/tsconfig.server.json | 12 + apps/vpn-manager/types.d.ts | 4 + docker-compose.yml | 15 +- package.json | 1 + pnpm-lock.yaml | 1192 +++++++++++------ turbo.json | 1 + 50 files changed, 3026 insertions(+), 376 deletions(-) create mode 100644 Dockerfile.vpn-manager create mode 100644 apps/vpn-manager/.gitignore create mode 100644 apps/vpn-manager/README.md create mode 100644 apps/vpn-manager/next.config.mjs create mode 100644 apps/vpn-manager/package.json create mode 100644 apps/vpn-manager/public/next.svg create mode 100644 apps/vpn-manager/public/vercel.svg create mode 100644 apps/vpn-manager/sentry.client.config.ts create mode 100644 apps/vpn-manager/sentry.edge.config.ts create mode 100644 apps/vpn-manager/sentry.server.config.ts create mode 100644 apps/vpn-manager/src/assets/icons/Type=github, Size=24, Color=CurrentColor.svg create mode 100644 apps/vpn-manager/src/assets/icons/Type=x, Size=24, Color=CurrentColor.svg create mode 100644 apps/vpn-manager/src/assets/icons/menu-icon.svg create mode 100644 apps/vpn-manager/src/components/DesktopNavBar/DesktopNavBar.tsx create mode 100644 apps/vpn-manager/src/components/DesktopNavBar/index.ts create mode 100644 apps/vpn-manager/src/components/MobileNavBar/MobileNavBar.tsx create mode 100644 apps/vpn-manager/src/components/MobileNavBar/index.ts create mode 100644 apps/vpn-manager/src/components/NavBar/NavBar.tsx create mode 100644 apps/vpn-manager/src/components/NavBar/index.ts create mode 100644 apps/vpn-manager/src/components/NavBarNavList/NavBarNavList.tsx create mode 100644 apps/vpn-manager/src/components/NavBarNavList/index.ts create mode 100644 apps/vpn-manager/src/components/NavListItem/NavListItem.tsx create mode 100644 apps/vpn-manager/src/components/NavListItem/index.ts create mode 100644 apps/vpn-manager/src/components/NextImageButton/NextImageButton.tsx create mode 100644 apps/vpn-manager/src/components/NextImageButton/index.ts create mode 100644 apps/vpn-manager/src/components/Page/Page.tsx create mode 100644 apps/vpn-manager/src/components/Page/index.ts create mode 100644 apps/vpn-manager/src/favicon.ico create mode 100644 apps/vpn-manager/src/globals.css create mode 100644 apps/vpn-manager/src/layout.tsx create mode 100644 apps/vpn-manager/src/lib/data/spreadsheet.ts create mode 100644 apps/vpn-manager/src/lib/processNewHires.ts create mode 100644 apps/vpn-manager/src/page.module.css create mode 100644 apps/vpn-manager/src/pages/_app.tsx create mode 100644 apps/vpn-manager/src/pages/_document.tsx create mode 100644 apps/vpn-manager/src/pages/index.tsx create mode 100644 apps/vpn-manager/src/theme/fonts.css create mode 100644 apps/vpn-manager/src/theme/index.ts create mode 100644 apps/vpn-manager/src/types.d.ts create mode 100644 apps/vpn-manager/src/utils/createEmotionCache.ts create mode 100644 apps/vpn-manager/src/utils/index.ts create mode 100644 apps/vpn-manager/src/utils/wordToCamelCase.ts create mode 100644 apps/vpn-manager/tsconfig.json create mode 100644 apps/vpn-manager/tsconfig.server.json create mode 100644 apps/vpn-manager/types.d.ts diff --git a/.gitignore b/.gitignore index 74d544686..e1b5dc3dc 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ apps/promisetracker/public/data/** storybook-static mongo-keyfile + +#Google credentials +credentials.json diff --git a/Dockerfile.vpn-manager b/Dockerfile.vpn-manager new file mode 100644 index 000000000..3f0b197f2 --- /dev/null +++ b/Dockerfile.vpn-manager @@ -0,0 +1,69 @@ +FROM node:18-alpine as node-alpine + +# Always install security updated e.g. https://pythonspeed.com/articles/security-updates-in-docker/ +# Update local cache so that other stages don't need to update cache +RUN apk update \ + && apk upgrade + + +FROM node-alpine as base + +RUN apk add --no-cache libc6-compat + +ARG PNPM_VERSION=8.5.0 + +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate + +WORKDIR /workspace + +COPY pnpm-lock.yaml . + +RUN pnpm fetch + + +FROM base as builder + + +WORKDIR /workspace + +COPY *.yaml *.json ./ +COPY packages ./packages +COPY apps/vpn-manager ./apps/vpn-manager + +# Use virtual store: https://pnpm.io/cli/fetch#usage-scenario +RUN pnpm install --recursive --offline --frozen-lockfile + +# NOTE: ARG values are only available **after** ARG statement & hence we need +# to separate NEXT_PUBLIC_APP_URL and PAYLOAD_PUBLIC_APP_URL into +# multiple ARG statements so that PAYLOAD can use the value defined +# in NEXT. +ARG PORT=3000 \ + # Sentry config for source maps upload (needed at build time only) + SENTRY_AUTH_TOKEN="" \ + SENTRY_ENV="local" \ + SENTRY_ORG="" \ + SENTRY_PROJECT="" \ + NEXT_APP_SENTRY_DSN="" +RUN pnpm build-ts --filter=vpn-manager +RUN pnpm build --filter=vpn-manager + +FROM builder as runner + +RUN rm -rf /var/cache/apk/* + +ARG PORT \ + SENTRY_ENV + +ENV NODE_ENV=production \ + PORT=${PORT} \ + SENTRY_ENV=${SENTRY_ENV} \ + NEXT_APP_SENTRY_DSN=${NEXT_APP_SENTRY_DSN} \ + SENTRY_ORG=${SENTRY_ORG} \ + SENTRY_PROJECT=${SENTRY_PROJECT} \ + SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} + +WORKDIR /workspace/apps/vpn-manager + +EXPOSE ${PORT} + +CMD [ "pnpm", "run", "start" ] diff --git a/apps/vpn-manager/.gitignore b/apps/vpn-manager/.gitignore new file mode 100644 index 000000000..0ea48f883 --- /dev/null +++ b/apps/vpn-manager/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Sentry Config File +.sentryclirc diff --git a/apps/vpn-manager/README.md b/apps/vpn-manager/README.md new file mode 100644 index 000000000..c4033664f --- /dev/null +++ b/apps/vpn-manager/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/vpn-manager/next.config.mjs b/apps/vpn-manager/next.config.mjs new file mode 100644 index 000000000..ea793582e --- /dev/null +++ b/apps/vpn-manager/next.config.mjs @@ -0,0 +1,38 @@ +import { withSentryConfig } from "@sentry/nextjs"; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ["@commons-ui/core", "@commons-ui/next"], + eslint: { + ignoreDuringBuilds: true, + }, + webpack: (config) => { + config.module.rules.push( + { + test: /\.svg$/i, + type: "asset", + resourceQuery: /url/, // *.svg?url + }, + { + test: /\.svg$/i, + issuer: /\.[jt]sx?$/, + resourceQuery: { not: [/url/] }, // exclude react component if *.svg?url + use: ["@svgr/webpack"], + }, + { + test: /\.md$/, + loader: "frontmatter-markdown-loader", + }, + ); + config.experiments = { ...config.experiments, topLevelAwait: true }; // eslint-disable-line no-param-reassign + return config; + }, +}; + +export default withSentryConfig(nextConfig, { + silent: true, + hideSourceMaps: true, + org: process.env.SENTRY_ORG, + authToken: process.env.SENTRY_AUTH_TOKEN, + project: process.env.SENTRY_PROJECT, +}); diff --git a/apps/vpn-manager/package.json b/apps/vpn-manager/package.json new file mode 100644 index 000000000..baf83c441 --- /dev/null +++ b/apps/vpn-manager/package.json @@ -0,0 +1,45 @@ +{ + "name": "vpn-manager", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "build-ts": "tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json", + "process-new-hires": "node dist/src/lib/processNewHires.js" + }, + "dependencies": { + "@babel/core": "^7.23.6", + "@babel/preset-react": "^7.23.3", + "@commons-ui/core": "workspace:*", + "@commons-ui/next": "workspace:*", + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.1", + "@emotion/server": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@mui/material": "^5.14.20", + "@mui/utils": "^5.14.20", + "@sentry/nextjs": "^7.105.0", + "@svgr/webpack": "^8.1.0", + "@types/jest": "^29.5.12", + "googleapis": "^133.0.0", + "jest": "^29.7.0", + "next": "14.1.3", + "react": "^18", + "react-dom": "^18", + "tsc-alias": "^1.8.8", + "tsconfig-paths": "^4.2.0" + }, + "devDependencies": { + "@commons-ui/testing-library": "workspace:*", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8.55.0", + "eslint-config-commons-ui": "workspace:*", + "eslint-import-resolver-webpack": "^0.13.8", + "eslint-plugin-import": "^2.29.0", + "typescript": "^5" + } +} diff --git a/apps/vpn-manager/public/next.svg b/apps/vpn-manager/public/next.svg new file mode 100644 index 000000000..5174b28c5 --- /dev/null +++ b/apps/vpn-manager/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/vpn-manager/public/vercel.svg b/apps/vpn-manager/public/vercel.svg new file mode 100644 index 000000000..d2f842227 --- /dev/null +++ b/apps/vpn-manager/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/vpn-manager/sentry.client.config.ts b/apps/vpn-manager/sentry.client.config.ts new file mode 100644 index 000000000..ee489fc9e --- /dev/null +++ b/apps/vpn-manager/sentry.client.config.ts @@ -0,0 +1,16 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.NEXT_APP_SENTRY_DSN, + tracesSampleRate: 1, + environment: process.env.SENTRY_ENV, + debug: false, + replaysOnErrorSampleRate: 1.0, + replaysSessionSampleRate: 0.1, + integrations: [ + Sentry.replayIntegration({ + maskAllText: true, + blockAllMedia: true, + }), + ], +}); diff --git a/apps/vpn-manager/sentry.edge.config.ts b/apps/vpn-manager/sentry.edge.config.ts new file mode 100644 index 000000000..ebb2dbda4 --- /dev/null +++ b/apps/vpn-manager/sentry.edge.config.ts @@ -0,0 +1,8 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.NEXT_APP_SENTRY_DSN, + environment: process.env.SENTRY_ENV, + tracesSampleRate: 1, + debug: false, +}); diff --git a/apps/vpn-manager/sentry.server.config.ts b/apps/vpn-manager/sentry.server.config.ts new file mode 100644 index 000000000..ebb2dbda4 --- /dev/null +++ b/apps/vpn-manager/sentry.server.config.ts @@ -0,0 +1,8 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.NEXT_APP_SENTRY_DSN, + environment: process.env.SENTRY_ENV, + tracesSampleRate: 1, + debug: false, +}); diff --git a/apps/vpn-manager/src/assets/icons/Type=github, Size=24, Color=CurrentColor.svg b/apps/vpn-manager/src/assets/icons/Type=github, Size=24, Color=CurrentColor.svg new file mode 100644 index 000000000..33fc4f5fb --- /dev/null +++ b/apps/vpn-manager/src/assets/icons/Type=github, Size=24, Color=CurrentColor.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/vpn-manager/src/assets/icons/Type=x, Size=24, Color=CurrentColor.svg b/apps/vpn-manager/src/assets/icons/Type=x, Size=24, Color=CurrentColor.svg new file mode 100644 index 000000000..93fbad2f7 --- /dev/null +++ b/apps/vpn-manager/src/assets/icons/Type=x, Size=24, Color=CurrentColor.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/vpn-manager/src/assets/icons/menu-icon.svg b/apps/vpn-manager/src/assets/icons/menu-icon.svg new file mode 100644 index 000000000..2a4912de7 --- /dev/null +++ b/apps/vpn-manager/src/assets/icons/menu-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/vpn-manager/src/components/DesktopNavBar/DesktopNavBar.tsx b/apps/vpn-manager/src/components/DesktopNavBar/DesktopNavBar.tsx new file mode 100644 index 000000000..5592c944b --- /dev/null +++ b/apps/vpn-manager/src/components/DesktopNavBar/DesktopNavBar.tsx @@ -0,0 +1,57 @@ +import { Grid, Box, Grid2Props } from "@mui/material"; +import React, { FC } from "react"; + +import NavBarNavList from "@/vpn-manager/components/NavBarNavList"; +import NextImageButton from "@/vpn-manager/components/NextImageButton"; + +interface SocialLinks { + platform: string; + url: string; +} + +interface Menu { + label: string; + href: string; +} +interface Props extends Grid2Props { + logo: any; + menus: Menu[]; + socialLinks: SocialLinks[]; +} +const DesktopNavBar: FC = React.forwardRef( + function DesktopNavBar(props, ref) { + const { logo, menus, socialLinks, sx } = props; + + return ( + + + + + + + + + + + ); + }, +); + +export default DesktopNavBar; diff --git a/apps/vpn-manager/src/components/DesktopNavBar/index.ts b/apps/vpn-manager/src/components/DesktopNavBar/index.ts new file mode 100644 index 000000000..3919164ee --- /dev/null +++ b/apps/vpn-manager/src/components/DesktopNavBar/index.ts @@ -0,0 +1,3 @@ +import DesktopNavBar from "./DesktopNavBar"; + +export default DesktopNavBar; diff --git a/apps/vpn-manager/src/components/MobileNavBar/MobileNavBar.tsx b/apps/vpn-manager/src/components/MobileNavBar/MobileNavBar.tsx new file mode 100644 index 000000000..028608672 --- /dev/null +++ b/apps/vpn-manager/src/components/MobileNavBar/MobileNavBar.tsx @@ -0,0 +1,139 @@ +import { + Dialog, + DialogContent, + Grid, + Grid2Props, + IconButton, + Slide, + SlideProps, + SvgIcon, +} from "@mui/material"; +import { styled } from "@mui/material/styles"; +import React, { FC, ForwardedRef } from "react"; + +import CloseIcon from "@/vpn-manager/assets/icons/Type=x, Size=24, Color=CurrentColor.svg"; +import menuIcon from "@/vpn-manager/assets/icons/menu-icon.svg"; +import NavBarNavList from "@/vpn-manager/components/NavBarNavList"; +import NextImageButton from "@/vpn-manager/components/NextImageButton"; + +interface SocialLinks { + platform: string; + url: string; +} + +interface Menu { + label: string; + href: string; +} +interface Props extends Grid2Props { + logo: any; + menus: Menu[]; + socialLinks: SocialLinks[]; +} + +const DialogContainer = styled(Dialog)(({ theme: { palette, spacing } }) => ({ + "& .MuiDialog-container": { + height: "100%", + }, + "& .MuiBackdrop-root": { + background: "transparent", + }, + "& .MuiDialogContent-root": { + padding: spacing(5), + color: palette.text.secondary, + background: palette.primary.main, + }, +})); + +const Transition: FC = React.forwardRef(function Transition( + { children, ...props }, + ref, +) { + return ( + + {children} + + ); +}); + +const MobileNavBar: FC = React.forwardRef(function MobileNavBar( + props, + ref: ForwardedRef, +) { + const { logo, menus, socialLinks, sx } = props; + const [open, setOpen] = React.useState(false); + + const handleClickOpen = () => { + setOpen(true); + }; + const handleClose = () => { + setOpen(false); + }; + + return ( + + + + + + + + + + + + + + + + + ); +}); + +export default MobileNavBar; diff --git a/apps/vpn-manager/src/components/MobileNavBar/index.ts b/apps/vpn-manager/src/components/MobileNavBar/index.ts new file mode 100644 index 000000000..b19184643 --- /dev/null +++ b/apps/vpn-manager/src/components/MobileNavBar/index.ts @@ -0,0 +1,3 @@ +import MobileNavBar from "./MobileNavBar"; + +export default MobileNavBar; diff --git a/apps/vpn-manager/src/components/NavBar/NavBar.tsx b/apps/vpn-manager/src/components/NavBar/NavBar.tsx new file mode 100644 index 000000000..22cee3e5c --- /dev/null +++ b/apps/vpn-manager/src/components/NavBar/NavBar.tsx @@ -0,0 +1,46 @@ +import { NavBar as NavigationBar, Section } from "@commons-ui/core"; +import React from "react"; + +import DesktopNavBar from "@/vpn-manager/components/DesktopNavBar"; +import MobileNavBar from "@/vpn-manager/components/MobileNavBar"; + +interface SocialLinks { + platform: string; + url: string; +} + +interface Menu { + label: string; + href: string; +} +interface Props { + logo: any; + menus: Menu[]; + socialLinks: SocialLinks[]; +} +function NavBar({ logo, menus, socialLinks }: Props) { + return ( + +
+ + +
+
+ ); +} + +export default NavBar; diff --git a/apps/vpn-manager/src/components/NavBar/index.ts b/apps/vpn-manager/src/components/NavBar/index.ts new file mode 100644 index 000000000..085b6b525 --- /dev/null +++ b/apps/vpn-manager/src/components/NavBar/index.ts @@ -0,0 +1,3 @@ +import NavBar from "./NavBar"; + +export default NavBar; diff --git a/apps/vpn-manager/src/components/NavBarNavList/NavBarNavList.tsx b/apps/vpn-manager/src/components/NavBarNavList/NavBarNavList.tsx new file mode 100644 index 000000000..ccc623794 --- /dev/null +++ b/apps/vpn-manager/src/components/NavBarNavList/NavBarNavList.tsx @@ -0,0 +1,103 @@ +import React, { ElementType, FC } from "react"; + +import GitHubIcon from "@/vpn-manager/assets/icons/Type=github, Size=24, Color=CurrentColor.svg"; +import NavListItem from "@/vpn-manager/components/NavListItem"; +import { NavList } from "@commons-ui/core"; +import { Link } from "@commons-ui/next"; +import { LinkProps, SvgIcon } from "@mui/material"; + +const platformToIconMap: { + [key: string]: ElementType; +} = { + Github: GitHubIcon, +}; + +interface NavListItemProps extends LinkProps {} + +interface SocialLinks { + platform: string; + url: string; +} + +interface Menu { + label: string; + href: string; +} + +interface Props { + NavListItemProps?: NavListItemProps; + direction?: string; + menus?: Menu[]; + socialLinks?: SocialLinks[]; +} + +const NavBarNavList: FC = React.forwardRef( + function NavBarNavList(props, ref) { + const { NavListItemProps, direction, menus, socialLinks, ...other } = props; + + if (!menus?.length) { + return null; + } + return ( + + {menus.map((item) => ( + + + {item.label} + + + ))} + {socialLinks?.map(({ platform, url }) => { + const Icon = platformToIconMap[platform]; + if (!Icon) { + return null; + } + return ( + + + + + + ); + })} + + ); + }, +); + +export default NavBarNavList; diff --git a/apps/vpn-manager/src/components/NavBarNavList/index.ts b/apps/vpn-manager/src/components/NavBarNavList/index.ts new file mode 100644 index 000000000..f261c9be0 --- /dev/null +++ b/apps/vpn-manager/src/components/NavBarNavList/index.ts @@ -0,0 +1,3 @@ +import NavBarNavList from "./NavBarNavList"; + +export default NavBarNavList; diff --git a/apps/vpn-manager/src/components/NavListItem/NavListItem.tsx b/apps/vpn-manager/src/components/NavListItem/NavListItem.tsx new file mode 100644 index 000000000..5cde403fc --- /dev/null +++ b/apps/vpn-manager/src/components/NavListItem/NavListItem.tsx @@ -0,0 +1,19 @@ +import { styled, SxProps } from "@mui/material/styles"; +import React, { FC, ForwardedRef, HTMLAttributes } from "react"; + +const NavListItemRoot = styled("li")({ + listStyle: "none", +}); + +interface Props extends HTMLAttributes { + sx?: SxProps; +} + +const NavListItem: FC = React.forwardRef(function NavListItem( + props, + ref: ForwardedRef, +) { + return ; +}); + +export default NavListItem; diff --git a/apps/vpn-manager/src/components/NavListItem/index.ts b/apps/vpn-manager/src/components/NavListItem/index.ts new file mode 100644 index 000000000..abc33a899 --- /dev/null +++ b/apps/vpn-manager/src/components/NavListItem/index.ts @@ -0,0 +1,3 @@ +import NavListItem from "./NavListItem"; + +export default NavListItem; diff --git a/apps/vpn-manager/src/components/NextImageButton/NextImageButton.tsx b/apps/vpn-manager/src/components/NextImageButton/NextImageButton.tsx new file mode 100644 index 000000000..e118ada07 --- /dev/null +++ b/apps/vpn-manager/src/components/NextImageButton/NextImageButton.tsx @@ -0,0 +1,40 @@ +import { ImageButton } from "@commons-ui/core"; +import { Link } from "@commons-ui/next"; +import Image from "next/image"; +import React, { FC } from "react"; + +interface Props { + src?: string; + href?: string; + alt: string; + width?: number; + height?: number; + priority?: boolean; + onClick?: () => void; +} + +const NextImageButton: FC = React.forwardRef(function Logo(props, ref) { + const { alt, height, href, priority, src, width, ...other } = props; + + if (!src) { + return null; + } + return ( + + {alt} + + ); +}); + +export default NextImageButton; diff --git a/apps/vpn-manager/src/components/NextImageButton/index.ts b/apps/vpn-manager/src/components/NextImageButton/index.ts new file mode 100644 index 000000000..9e6d32a68 --- /dev/null +++ b/apps/vpn-manager/src/components/NextImageButton/index.ts @@ -0,0 +1,3 @@ +import NextImageButton from "./NextImageButton"; + +export default NextImageButton; diff --git a/apps/vpn-manager/src/components/Page/Page.tsx b/apps/vpn-manager/src/components/Page/Page.tsx new file mode 100644 index 000000000..f09c86403 --- /dev/null +++ b/apps/vpn-manager/src/components/Page/Page.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +import NavBar from "@/vpn-manager/components/NavBar"; + +interface SocialLinks { + platform: string; + url: string; +} + +interface Menu { + label: string; + href: string; +} + +interface Menu { + label: string; + href: string; +} + +interface Navbar { + logo: any; + menus: Menu[]; + socialLinks: SocialLinks[]; +} + +interface Props { + children?: React.ReactNode; + navbar?: Navbar; +} +function Page({ children, navbar }: Props) { + return ( + <> + {navbar ? : null} + {children ?
{children}
: null} + + ); +} + +export default Page; diff --git a/apps/vpn-manager/src/components/Page/index.ts b/apps/vpn-manager/src/components/Page/index.ts new file mode 100644 index 000000000..39d0be934 --- /dev/null +++ b/apps/vpn-manager/src/components/Page/index.ts @@ -0,0 +1,3 @@ +import Page from "./Page"; + +export default Page; diff --git a/apps/vpn-manager/src/favicon.ico b/apps/vpn-manager/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8869420774030aa9a4bc16a8b1d119f38b891df3 GIT binary patch literal 15086 zcmd6u3v?9K8OLXKHiUP8T7lvzQA)80inJ;m+F}d#h{&U0J%UiF0RaIOft~`^1dw1L zAv^>X;qWdLENDyRfY?Sst(F$*lRytb@l*-|;h_cyng0Hp85q{x&CX^w;XCJ-Z|1(f zZ|{6_@0~jn!!QlY=+MDHXBul;8^*1MVPs}H=Zy?wKWUB7${!Ilj9dy_PZ_EhLqO>v z`A&tZ&cDIHo&vBVFr_;ENYs|c|F1ay0CqciWTI_WXFZSq?{j<|G=?Tnr#k&e)E0ia z@pGKxb+8gvdC>WC_(7sJQdtkbCc}T=6F3yc|2Qv)3(zl>e#F~$(Ar}{z`?{X#Y)Qv zTtixKI1N{X|M+3SuVFp>9uzOqYa_6Rv{&IgTq*ts@nsWeyetL9$Kfcrd7qMh5PC$p z#sSiGuZ#33Ws;JN4_^N_bKVLvE8$uvzit#KG?l#q+hHj*gp>$=c>Vu3=L=wdoT%(V zr;ha4I<;&IM{j{sV8h$c7*ZnGl>ZtJKZhf5ECCdEioYGTg0|vC{-1&U&avVtWrE!n zL2G}Tp!HenOZ=DPeQn75F0`(M4djRWe;S96!xDH7?uS%LXBwXcGaa3SzB*_fXpe6* zVO9*Z#_;;DdF=!!eq0uFoJtu<FXh%94_HyAd z81L{nIvQi0ykY1J@XE8CxYsn*@iQD-=iwZb`QQR&<>RMt9eAZXClefg=FLdKaSPh) z=9AkgKM$h$PgcsM2^JmzUH=DtGKBK0p(g!rkNsng4~hlFkY5yU2OvCtNJ_bQ{nyy9 z7*eeI<;T>~*je4;_5THo4~8KL@GI&J4MQ=h_ERZp{|J|Bq095d0Ln*D&99{i{slr|ZA2 zfv!Wk3{pnEVI6vOIAJq!Dh z0EM+rpuL+!xRH7m^U^ig?wkO(QAcz6VMxUPy4c?a+OPT2(K?O?J!9>JhWI}j`?~(6 z32@f&JrV!wVSfi`o%AmJ1}4VIB+6->o6g#%3$Zi_CdSK-iZSmAd>8ut(znr`AMyIX zfr>XnN1te~Rr`H@>V2Q`$#~ZF`mbl`FRB6U%PLm2?_fbyB#p{?#?*e_AcDAz_8 zsgp_H6-v0ImG1g;I%JpP=mSQxQ2HUKysHmTUj3p$uA+bJa%oMVuFlqZwkj$;<1($6 z+nI1XT>MU}5hqEMpjE8zBK`n+uXqmhj(0#3O~h=x^Wt$-N8@YrWllcJp+vbt<|a`SyW!{(2aNa_hCzYu+D?dk79q96xL*1=MTC1@{;Ie#8HfkZK= zdIw-IR7JEN$%Y)55&`9{1-+|!09;WVehbtWiHO#d!(jnD6N7o={TagTlT;n zgM644gQ?`HA8vwViECd|v)*MYrYmD1Wpv%8pM(rZRMh{2Kym+c7&AE+8sj4IG3on3 z*FP>{zv6BvbeMylv)wXhv$s(YMj`J_^gg-^%!N4-*h`vU`}x?^-r-W12|AC&Z1x9b zzcVN&s{0pdyA>F1l->eP3t$V0qwVH9*QNJ8-EGf8=Vw` zC&;@4AKKF9M3@u<~C5T|T6hU6y^G^KrHv$VO+|rEGVUvf)r>q}w^o(ru$z1`bIpgzKTmI`y-w&*n=q6x7P)*0tTm9rCiI}*V4$1u^Qsptz$6o&XQo@EuLkI zbAj);i)RO|PZ%>>O{A{Y+3KPwQolA&c=07uQ^iIGzgiLHEelgmF2| zPBYV+^Q{(z)@HC~t9NP5T0n&vpm}R>cxBZEtt0YZW6s`WnR$O?J-Xg9M>RE!5iIUz z!z|icMaPB4e#%-<+aUuz4>U*SGyjSL$a@7|=3M-V`D) { + return ( + + + + {children} + + + ); +} diff --git a/apps/vpn-manager/src/lib/data/spreadsheet.ts b/apps/vpn-manager/src/lib/data/spreadsheet.ts new file mode 100644 index 000000000..00f256b97 --- /dev/null +++ b/apps/vpn-manager/src/lib/data/spreadsheet.ts @@ -0,0 +1,59 @@ +import { google } from "googleapis"; + +import { SheetRow } from "@/vpn-manager/types"; +import { toCamelCase } from "@/vpn-manager/utils"; + +function gSheet() { + const auth = new google.auth.GoogleAuth({ + keyFile: process.env.NEXT_APP_GOOGLE_CREDENTIALS, + scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"], + }); + return google.sheets({ version: "v4", auth }); +} + +async function list( + spreadsheetId?: string, + range?: string, +): Promise { + if (!spreadsheetId || !range) { + return []; + } + const sheets = gSheet(); + const response = await sheets.spreadsheets.values.get({ + spreadsheetId, + range, + }); + + const rows = response.data.values; + if (!rows || !rows?.length) { + return []; + } + const titles = rows[0]; + + const data = rows.slice(1).map((row: any) => { + return titles.reduce((acc, curr, index) => { + const key = toCamelCase(curr); + const value = row[index]; + return { + ...acc, + [key]: value, + }; + }, {}); + }); + + return data; +} + +async function newHires() { + const spreadsheetId = process.env.NEXT_APP_GOOGLE_SHEET_ID; + const range = process.env.NEXT_APP_GOOGLE_SHEET_RANGE; + const data = await list(spreadsheetId, range); + return data.filter( + (row: SheetRow) => row.emailAddress && row.keySent !== "Yes", + ); +} + +export default { + list, + newHires, +}; diff --git a/apps/vpn-manager/src/lib/processNewHires.ts b/apps/vpn-manager/src/lib/processNewHires.ts new file mode 100644 index 000000000..a9f208b9b --- /dev/null +++ b/apps/vpn-manager/src/lib/processNewHires.ts @@ -0,0 +1,17 @@ +import { SheetRow } from "@/vpn-manager/types"; +import * as Sentry from "@sentry/nextjs"; + +import spreadsheet from "./data/spreadsheet"; + +export async function processEmployee(item: SheetRow) { + // Capture to test that it works + Sentry.captureException(item); +} + +export async function processNewHires() { + const newHires = await spreadsheet.newHires(); + const promises = newHires.map((item) => processEmployee(item)); + Promise.allSettled(promises); +} + +processNewHires(); diff --git a/apps/vpn-manager/src/page.module.css b/apps/vpn-manager/src/page.module.css new file mode 100644 index 000000000..d979f776c --- /dev/null +++ b/apps/vpn-manager/src/page.module.css @@ -0,0 +1,232 @@ +.main { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + padding: 6rem; + min-height: 100vh; +} + +.description { + display: inherit; + justify-content: inherit; + align-items: inherit; + font-size: 0.85rem; + max-width: var(--max-width); + width: 100%; + z-index: 2; + font-family: var(--font-mono); +} + +.description a { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; +} + +.description p { + position: relative; + margin: 0; + padding: 1rem; + background-color: rgba(var(--callout-rgb), 0.5); + border: 1px solid rgba(var(--callout-border-rgb), 0.3); + border-radius: var(--border-radius); +} + +.code { + font-weight: 700; + font-family: var(--font-mono); +} + +.grid { + display: grid; + grid-template-columns: repeat(4, minmax(25%, auto)); + max-width: 100%; + width: var(--max-width); +} + +.card { + padding: 1rem 1.2rem; + border-radius: var(--border-radius); + background: rgba(var(--card-rgb), 0); + border: 1px solid rgba(var(--card-border-rgb), 0); + transition: + background 200ms, + border 200ms; +} + +.card span { + display: inline-block; + transition: transform 200ms; +} + +.card h2 { + font-weight: 600; + margin-bottom: 0.7rem; +} + +.card p { + margin: 0; + opacity: 0.6; + font-size: 0.9rem; + line-height: 1.5; + max-width: 30ch; + text-wrap: balance; +} + +.center { + display: flex; + justify-content: center; + align-items: center; + position: relative; + padding: 4rem 0; +} + +.center::before { + background: var(--secondary-glow); + border-radius: 50%; + width: 480px; + height: 360px; + margin-left: -400px; +} + +.center::after { + background: var(--primary-glow); + width: 240px; + height: 180px; + z-index: -1; +} + +.center::before, +.center::after { + content: ""; + left: 50%; + position: absolute; + filter: blur(45px); + transform: translateZ(0); +} + +.logo { + position: relative; +} +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + .card:hover { + background: rgba(var(--card-rgb), 0.1); + border: 1px solid rgba(var(--card-border-rgb), 0.15); + } + + .card:hover span { + transform: translateX(4px); + } +} + +@media (prefers-reduced-motion) { + .card:hover span { + transform: none; + } +} + +/* Mobile */ +@media (max-width: 700px) { + .content { + padding: 4rem; + } + + .grid { + grid-template-columns: 1fr; + margin-bottom: 120px; + max-width: 320px; + text-align: center; + } + + .card { + padding: 1rem 2.5rem; + } + + .card h2 { + margin-bottom: 0.5rem; + } + + .center { + padding: 8rem 0 6rem; + } + + .center::before { + transform: none; + height: 300px; + } + + .description { + font-size: 0.8rem; + } + + .description a { + padding: 1rem; + } + + .description p, + .description div { + display: flex; + justify-content: center; + position: fixed; + width: 100%; + } + + .description p { + align-items: center; + inset: 0 0 auto; + padding: 2rem 1rem 1.4rem; + border-radius: 0; + border: none; + border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); + background: linear-gradient( + to bottom, + rgba(var(--background-start-rgb), 1), + rgba(var(--callout-rgb), 0.5) + ); + background-clip: padding-box; + backdrop-filter: blur(24px); + } + + .description div { + align-items: flex-end; + pointer-events: none; + inset: auto 0 0; + padding: 2rem; + height: 200px; + background: linear-gradient( + to bottom, + transparent 0%, + rgb(var(--background-end-rgb)) 40% + ); + z-index: 1; + } +} + +/* Tablet and Smaller Desktop */ +@media (min-width: 701px) and (max-width: 1120px) { + .grid { + grid-template-columns: repeat(2, 50%); + } +} + +@media (prefers-color-scheme: dark) { + .vercelLogo { + filter: invert(1); + } + + .logo { + filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); + } +} + +@keyframes rotate { + from { + transform: rotate(360deg); + } + to { + transform: rotate(0deg); + } +} diff --git a/apps/vpn-manager/src/pages/_app.tsx b/apps/vpn-manager/src/pages/_app.tsx new file mode 100644 index 000000000..3f62ecf74 --- /dev/null +++ b/apps/vpn-manager/src/pages/_app.tsx @@ -0,0 +1,39 @@ +import "@/vpn-manager/theme/fonts.css"; + +import React, { ReactNode } from "react"; + +import { AppProps } from "next/app"; +import Head from "next/head"; + +import Page from "@/vpn-manager/components/Page"; +import theme from "@/vpn-manager/theme"; +import createEmotionCache from "@/vpn-manager/utils/createEmotionCache"; +import { CacheProvider } from "@emotion/react"; +import { CssBaseline, ThemeProvider } from "@mui/material"; + +const clientSideEmotionCache = createEmotionCache(); + +function getDefaultLayout(page: ReactNode, pageProps: any) { + return {page}; +} + +function MyApp(props: AppProps | any) { + const { Component, emotionCache = clientSideEmotionCache, pageProps } = props; + const getLayout = Component.getLayout || getDefaultLayout; + + return ( + <> + + + + + + + {getLayout(, pageProps)} + + + + ); +} + +export default MyApp; diff --git a/apps/vpn-manager/src/pages/_document.tsx b/apps/vpn-manager/src/pages/_document.tsx new file mode 100644 index 000000000..846b881d5 --- /dev/null +++ b/apps/vpn-manager/src/pages/_document.tsx @@ -0,0 +1,120 @@ +import React from "react"; + +import Document, { Head, Html, Main, NextScript } from "next/document"; + +import createEmotionCache from "@/vpn-manager/utils/createEmotionCache"; +import createEmotionServer from "@emotion/server/create-instance"; + +class MyDocument extends Document { + render() { + return ( + + + + + + + + + + + + + + {this.props.emotionStyleTags} + + +
+ + + + ); + } +} + +// `getInitialProps` belongs to `_document` (instead of `_app`), +// it's compatible with static-site generation (SSG). +MyDocument.getInitialProps = async (ctx) => { + // Resolution order + // + // On the server: + // 1. app.getInitialProps + // 2. page.getInitialProps + // 3. document.getInitialProps + // 4. app.render + // 5. page.render + // 6. document.render + // + // On the server with error: + // 1. document.getInitialProps + // 2. app.render + // 3. page.render + // 4. document.render + // + // On the client + // 1. app.getInitialProps + // 2. page.getInitialProps + // 3. app.render + // 4. page.render + + const originalRenderPage = ctx.renderPage; + + // You can consider sharing the same Emotion cache between all the SSR requests to speed up performance. + // However, be aware that it can have global side effects. + const cache = createEmotionCache(); + const { extractCriticalToChunks } = createEmotionServer(cache); + + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: (App: any) => + function EnhanceApp(props) { + return ; + }, + }); + + const initialProps = await Document.getInitialProps(ctx); + // This is important. It prevents Emotion to render invalid HTML. + // See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 + const emotionStyles = extractCriticalToChunks(initialProps.html); + const emotionStyleTags = emotionStyles.styles.map((style: any) => ( +