diff --git a/.github/workflows/roboshield-deploy-dev.yml b/.github/workflows/roboshield-deploy-dev.yml new file mode 100644 index 000000000..f248cf540 --- /dev/null +++ b/.github/workflows/roboshield-deploy-dev.yml @@ -0,0 +1,76 @@ +name: RoboShield | Deploy | DEV + +on: + push: + branches: [main] + paths: + - "apps/roboshield/**" + - "Dockerfile.roboshield" + - ".github/workflows/roboshield-dev.yml" + +concurrency: + group: "${{ github.workflow }} @ ${{ github.ref }}" + cancel-in-progress: true + +env: + DOKKU_REMOTE_BRANCH: "master" + DOKKU_REMOTE_URL: "ssh://azureuser@ui-1.dev.codeforafrica.org" + IMAGE_NAME: "codeforafrica/roboshield" + NEXT_PUBLIC_APP_URL: "https://roboshield-ui.dev.codeforafrica.org" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APP_NAME: roboshield-ui + +jobs: + deploy: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node-version: [20] + os: [ubuntu-latest] + steps: + - name: Cloning repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + key: ${{ runner.os }}-buildx-${{ github.sha }} + path: /tmp/.buildx-cache + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + username: ${{ secrets.DOCKER_HUB_USERNAME }} + + - name: Build Docker image + uses: docker/build-push-action@v3 + with: + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + context: . + file: Dockerfile.roboshield + push: true + tags: "${{ env.IMAGE_NAME }}:${{ github.sha }}" + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + - name: Push to Dokku + uses: dokku/github-action@v1.4.0 + with: + git_remote_url: ${{ env.DOKKU_REMOTE_URL }}/${{ env.APP_NAME }} + ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} + deploy_docker_image: ${{ env.IMAGE_NAME }}:${{ github.sha }} diff --git a/.gitignore b/.gitignore index 02a3dabd2..898fcc87c 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,7 @@ credentials.json # Sentry Config File .sentryclirc + +# SQLite files +**.db +**.sqlite diff --git a/Dockerfile.roboshield b/Dockerfile.roboshield new file mode 100644 index 000000000..e98cc72ba --- /dev/null +++ b/Dockerfile.roboshield @@ -0,0 +1,116 @@ +FROM node:20.14-alpine as node + +# 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 + +# =================================================== +# base: starting image to be used in all other stages +# =================================================== +FROM node as base + +# 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 /workspace + + +# =================================================== +# pnpm-base: starting image with pnpm activated. +# should be used in all "build" stages. +# =================================================== +FROM base as pnpm-base + +ARG PNPM_VERSION=9.1.4 + +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate + +# ========================================================= +# desp: image with all dependencies (for the app) installed +# ========================================================= +FROM pnpm-base as deps + +COPY pnpm-lock.yaml . + +RUN pnpm fetch + +COPY *.yaml *.json ./ +COPY packages/commons-ui-core/package.json ./packages/commons-ui-core/package.json +COPY packages/commons-ui-next/package.json ./packages/commons-ui-next/package.json +COPY apps/roboshield/package.json ./apps/roboshield/package.json + +# Use virtual store: https://pnpm.io/cli/fetch#usage-scenario +RUN pnpm --filter "./apps/roboshield" install --offline --frozen-lockfile + + +# ======================================================= +# builder: image that uses deps to build shippable output +# ======================================================= +FROM pnpm-base as builder + +ARG NEXT_TELEMETRY_DISABLED=1 \ + # Needed by Next.js at build time + NEXT_PUBLIC_APP_NAME=RoboShield \ + NEXT_PUBLIC_APP_URL=http://localhost:3000 \ + NEXT_PUBLIC_SEO_DISABLED="true" \ + # Needed by Next.js and server.ts at build time + PORT=3000 + +COPY --from=deps /workspace/node_modules ./node_modules +COPY --from=deps /workspace/packages/commons-ui-core/node_modules ./packages/commons-ui-core/node_modules +COPY --from=deps /workspace/packages/commons-ui-next/node_modules ./packages/commons-ui-next/node_modules +COPY --from=deps /workspace/apps/roboshield/node_modules ./apps/roboshield/node_modules + +COPY packages/commons-ui-core ./packages/commons-ui-core +COPY packages/commons-ui-next ./packages/commons-ui-next +COPY apps/roboshield ./apps/roboshield + +RUN pnpm --filter "./apps/roboshield" build + + +# ============================== +# runner: final deployable image +# ============================== +FROM base as runner + +ARG NEXT_TELEMETRY_DISABLED \ + NEXT_PUBLIC_APP_NAME \ + NEXT_PUBLIC_APP_URL \ + PORT + +ENV NODE_ENV=production \ + NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME} \ + NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} \ + PORT=${PORT} + +RUN set -ex \ + # Create a non-root user + && addgroup --system -g 1001 nodejs \ + && adduser --system -u 1001 -g 1001 nextjs \ + # Create nextjs cache dir w/ correct permissions + && mkdir -p ./apps/roboshield/.next \ + && chown nextjs:nodejs ./apps/roboshield/.next \ + # Delete system cached files we don't need anymore + && rm -rf /var/cache/apk/* + +# PNPM symlink some dependencies +COPY --from=builder --chown=nextjs:nodejs /workspace/node_modules ./node_modules +# Public assets +COPY --from=builder --chown=nextjs:nodejs /workspace/apps/roboshield/public ./apps/roboshield/public + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /workspace/apps/roboshield/.next/standalone ./apps/roboshield +COPY --from=builder --chown=nextjs:nodejs /workspace/apps/roboshield/.next/static ./apps/roboshield/.next/static + +USER nextjs + +EXPOSE ${PORT} + +# set hostname to localhost +ENV HOSTNAME "0.0.0.0" + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD ["node", "apps/roboshield/server.js"] diff --git a/Makefile b/Makefile index a5caad6fa..af180c58c 100644 --- a/Makefile +++ b/Makefile @@ -18,5 +18,9 @@ mongodb-keyfile: openssl rand -base64 741 > ./mongo-keyfile chmod 600 ./mongo-keyfile + pesayetu: $(COMPOSE_BUILD_ENV) $(COMPOSE) --env-file apps/pesayetu/.env.local up pesayetu --build + +roboshield: + $(COMPOSE_BUILD_ENV) $(COMPOSE) --env-file apps/roboshield/.env.local up roboshield --build diff --git a/README.md b/README.md index 7860ca231..3e05843fe 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ | [**charterAFRICA**](./apps/charterafrica/) | The largest digital database for communities | | [**Code for Africa**](./apps/codeforafrica/) | Africa's largest network of civic tech and open data labs | | [**PesaYetu**](./apps/pesayetu/) | Data to hold your government accountable | +| [**RoboShield**](./apps/roboshield/) | Guard your website against AI Bots | ## Get started diff --git a/apps/roboshield/.eslintignore b/apps/roboshield/.eslintignore new file mode 100644 index 000000000..19d6cb9ac --- /dev/null +++ b/apps/roboshield/.eslintignore @@ -0,0 +1,36 @@ +# dependencies +node_modules +.pnp +.pnp.js +.pnpm-debug.log + +# typescript +dist/ + +# testing +coverage + +# next.js +.next/ +out/ + +# payload +build/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Vercel +.vercel +.now + +# turbo +.turbo +test-results/ +playwright-report/ diff --git a/apps/roboshield/.eslintrc.js b/apps/roboshield/.eslintrc.js new file mode 100644 index 000000000..6c9302824 --- /dev/null +++ b/apps/roboshield/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + root: true, + extends: ["next/core-web-vitals", "plugin:prettier/recommended"], + settings: { + "import/resolver": { + webpack: { + config: "./eslint.webpack.config.js", + }, + }, + }, +}; diff --git a/apps/roboshield/.lintstagedrc.js b/apps/roboshield/.lintstagedrc.js new file mode 100644 index 000000000..32975639b --- /dev/null +++ b/apps/roboshield/.lintstagedrc.js @@ -0,0 +1,14 @@ +const path = require("path"); + +const buildEslintCommand = (filenames) => + `next lint --fix --file ${filenames + .map((f) => path.relative(process.cwd(), f)) + .join(" --file ")}`; + +module.exports = { + // Since we don't have eslint json/md plugins installed in this app, we can't + // use the eslint to lint json,md here + "*.{json,md}": ["prettier --write"], + "*.{yaml,yml}": "prettier --write", + "*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}": [buildEslintCommand], +}; diff --git a/apps/roboshield/README.md b/apps/roboshield/README.md new file mode 100644 index 000000000..cd090f290 --- /dev/null +++ b/apps/roboshield/README.md @@ -0,0 +1,17 @@ +RoboShield is a web application that allows users to create and manage their `robots.txt` files. + +## Getting Started + +### Install dependencies + +```bash +pnpm install +``` + +First, run the development server: + +```bash +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/apps/roboshield/eslint.webpack.config.js b/apps/roboshield/eslint.webpack.config.js new file mode 100644 index 000000000..650a330e0 --- /dev/null +++ b/apps/roboshield/eslint.webpack.config.js @@ -0,0 +1,25 @@ +const path = require("path"); + +module.exports = { + module: { + rules: [ + { + 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"], + }, + ], + }, + resolve: { + alias: { + "@/roboshield": path.resolve(__dirname, "src/"), + }, + extensions: [".ts", ".tsx"], + }, +}; diff --git a/apps/roboshield/next-env.d.ts b/apps/roboshield/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/apps/roboshield/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/apps/roboshield/next.config.mjs b/apps/roboshield/next.config.mjs new file mode 100644 index 000000000..85d7abfdf --- /dev/null +++ b/apps/roboshield/next.config.mjs @@ -0,0 +1,35 @@ +import path from "path"; + +const PROJECT_ROOT = process.env.PROJECT_ROOT?.trim(); +const outputFileTracingRoot = PROJECT_ROOT + ? path.resolve(__dirname, PROJECT_ROOT) + : undefined; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ["@commons-ui/core", "@commons-ui/next"], + reactStrictMode: true, + output: "standalone", + experimental: { + outputFileTracingRoot, + }, + 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"], + }, + ); + config.experiments = { ...config.experiments, topLevelAwait: true }; // eslint-disable-line no-param-reassign + return config; + }, +}; + +export default nextConfig; diff --git a/apps/roboshield/package.json b/apps/roboshield/package.json new file mode 100644 index 000000000..1e5bf3b9a --- /dev/null +++ b/apps/roboshield/package.json @@ -0,0 +1,56 @@ +{ + "name": "roboshield", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build", + "clean": "rm -rf .next .turbo node_modules", + "dev": "next dev", + "jest": "jest --passWithNoTests", + "lint": "TIMING=1 eslint --fix './'", + "lint-check": "TIMING=1 eslint './'", + "start": "next start" + }, + "dependencies": { + "@commons-ui/core": "workspace:*", + "@commons-ui/next": "workspace:*", + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.4", + "@emotion/server": "^11.11.0", + "@emotion/styled": "^11.11.5", + "@mui/icons-material": "^5.15.19", + "@mui/material": "^5.15.19", + "@mui/utils": "^5.15.14", + "@mui/x-date-pickers": "^7.6.2", + "@next/env": "^14.2.3", + "ace-builds": "^1.34.2", + "crawler-user-agents": "^1.0.142", + "date-fns": "^3.6.0", + "next": "14.2.4", + "react": "^18.3.1", + "react-ace": "^11.0.1", + "react-dom": "^18.3.1", + "react-rotating-text": "^1.4.1", + "robots-txt-parse": "^2.0.1", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", + "tsc-alias": "^1.8.10", + "tsconfig-paths": "^4.2.0" + }, + "devDependencies": { + "@commons-ui/testing-library": "workspace:*", + "@svgr/webpack": "^8.1.0", + "@types/node": "^20.14.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "babel-jest": "^29.7.0", + "eslint": "^8.57.0", + "eslint-config-next": "14.2.3", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-webpack": "^0.13.8", + "eslint-plugin-import": "^2.29.1", + "jest": "^29.7.0", + "jest-config-commons-ui": "workspace:*", + "typescript": "^5.4.5" + } +} diff --git a/apps/roboshield/public/bg-shape-7.svg b/apps/roboshield/public/bg-shape-7.svg new file mode 100644 index 000000000..e34af65f1 --- /dev/null +++ b/apps/roboshield/public/bg-shape-7.svg @@ -0,0 +1,41 @@ + + bg-shape-6-svg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/roboshield/public/bg-shape-8.svg b/apps/roboshield/public/bg-shape-8.svg new file mode 100644 index 000000000..25dfd7ecf --- /dev/null +++ b/apps/roboshield/public/bg-shape-8.svg @@ -0,0 +1,48 @@ + + bg-shape-7-svg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/roboshield/public/favicon.ico b/apps/roboshield/public/favicon.ico new file mode 100644 index 000000000..886942077 Binary files /dev/null and b/apps/roboshield/public/favicon.ico differ diff --git a/apps/roboshield/public/images/DW.png b/apps/roboshield/public/images/DW.png new file mode 100644 index 000000000..2b4df949a Binary files /dev/null and b/apps/roboshield/public/images/DW.png differ diff --git a/apps/roboshield/public/images/civic-signal.png b/apps/roboshield/public/images/civic-signal.png new file mode 100644 index 000000000..f83010098 Binary files /dev/null and b/apps/roboshield/public/images/civic-signal.png differ diff --git a/apps/roboshield/src/assets/icons/Type=github, Size=24, Color=CurrentColor.svg b/apps/roboshield/src/assets/icons/Type=github, Size=24, Color=CurrentColor.svg new file mode 100644 index 000000000..33fc4f5fb --- /dev/null +++ b/apps/roboshield/src/assets/icons/Type=github, Size=24, Color=CurrentColor.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/roboshield/src/assets/icons/Type=x, Size=24, Color=CurrentColor.svg b/apps/roboshield/src/assets/icons/Type=x, Size=24, Color=CurrentColor.svg new file mode 100644 index 000000000..93fbad2f7 --- /dev/null +++ b/apps/roboshield/src/assets/icons/Type=x, Size=24, Color=CurrentColor.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/roboshield/src/assets/icons/menu-icon.svg b/apps/roboshield/src/assets/icons/menu-icon.svg new file mode 100644 index 000000000..2a4912de7 --- /dev/null +++ b/apps/roboshield/src/assets/icons/menu-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/roboshield/src/components/Code/Code.tsx b/apps/roboshield/src/components/Code/Code.tsx new file mode 100644 index 000000000..2c9fefff5 --- /dev/null +++ b/apps/roboshield/src/components/Code/Code.tsx @@ -0,0 +1,99 @@ +import { Box, Button, Stack } from "@mui/material"; + +import CodeEditor from "./CodeEditor"; + +interface CodeProps { + code: string; + onCodeChange: (newCode: string) => void; + onCopy: () => void; + onDownload: () => void; + onReset: () => void; + onBack: () => void; + showButtons?: boolean; +} + +export default function Code(props: CodeProps) { + const { + code, + onCopy, + onDownload, + onReset, + onCodeChange, + onBack, + showButtons = false, + } = props; + + const handleCodeChange = (newCode: string) => { + onCodeChange(newCode); + }; + return ( + + + + + + + + + + + ); +} diff --git a/apps/roboshield/src/components/Code/CodeEditor.tsx b/apps/roboshield/src/components/Code/CodeEditor.tsx new file mode 100644 index 000000000..7e688f593 --- /dev/null +++ b/apps/roboshield/src/components/Code/CodeEditor.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import AceEditor from "react-ace"; + +import "ace-builds/src-noconflict/mode-python"; +import "ace-builds/src-noconflict/theme-textmate"; + +function CodeEditor({ + code, + setCode, + readOnly, +}: { + code: string; + setCode: any; + readOnly: boolean; +}) { + return ( + setCode(newCode)} + name="code-editor" + editorProps={{ $blockScrolling: true }} + showGutter={false} + showPrintMargin={false} + readOnly={readOnly} + value={code} + style={{ + width: "100%", + height: "500px", + border: readOnly + ? "1px solid rgb(19 81 216 / 10%)" + : "1px solid #C4C4C4", + background: readOnly + ? "linear-gradient(0deg, rgba(19, 81, 216, 0.01), rgba(19, 81, 216, 0.01)), linear-gradient(0deg, rgba(19, 81, 216, 0.05), rgba(19, 81, 216, 0.05))" + : "#FFFFFF", + + marginBottom: "10px", + borderRadius: "5px", + }} + /> + ); +} + +export default CodeEditor; diff --git a/apps/roboshield/src/components/Code/index.ts b/apps/roboshield/src/components/Code/index.ts new file mode 100644 index 000000000..c0b0dfc7c --- /dev/null +++ b/apps/roboshield/src/components/Code/index.ts @@ -0,0 +1,3 @@ +import Code from "./Code"; + +export default Code; diff --git a/apps/roboshield/src/components/CommonBots/CommonBots.tsx b/apps/roboshield/src/components/CommonBots/CommonBots.tsx new file mode 100644 index 000000000..b591b2b51 --- /dev/null +++ b/apps/roboshield/src/components/CommonBots/CommonBots.tsx @@ -0,0 +1,213 @@ +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import InfoIcon from "@mui/icons-material/Info"; +import WarningIcon from "@mui/icons-material/Warning"; +import { + Box, + Accordion, + AccordionSummary, + AccordionDetails, + Stack, + Typography, + Switch, + Grid, +} from "@mui/material"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import FormGroup from "@mui/material/FormGroup"; +import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; +import { useCallback, useState } from "react"; +import { useMemo, memo } from "react"; + +import StepperNav from "@/roboshield/components/StepperNav"; +import { useGlobalState } from "@/roboshield/context/GlobalContext"; +import { + Robot, + getBotType, + groupAndSortRobots, +} from "@/roboshield/lib/robots-data"; +import { StepComponent } from "@/roboshield/types/stepComponent"; + +export default function CommonBots({ + handleNext, + handleBack, + lastStep, +}: StepComponent) { + const { state } = useGlobalState(); + + const [selectedBots, setSelectedBots] = useState(state.bots); + + const MemoizedFormControlLabel = memo(FormControlLabel); + const robotsGroupedByType = useMemo( + () => Object.entries(groupAndSortRobots()), + [], + ); + + const isSelected = (robot: Robot) => { + return selectedBots.find((bot) => bot.name === robot.name)?.allow; + }; + + const toggleBot = useCallback((robot: Robot) => { + setSelectedBots((prev) => + prev.map((bot) => + bot.name === robot.name ? { ...bot, allow: !bot.allow } : bot, + ), + ); + }, []); + + const bulkToggle = useCallback((robots: Robot[], allow: boolean) => { + const robotNames = new Set(robots.map((robot) => robot.name)); + + setSelectedBots((prev) => + prev.map((bot) => + robotNames.has(bot.name) ? { ...bot, allow: !allow } : bot, + ), + ); + }, []); + + const isSwitchChecked = (robots: Robot[]) => { + return robots.every((robot) => !isSelected(robot)); + }; + + const next = () => { + handleNext({ bots: selectedBots }); + }; + + return ( + <> + + { + + + {robotsGroupedByType.map(([type, robots]) => ( + + } + aria-controls="panel1a-content" + id="panel1a-header" + sx={{ + background: "#E9EEFB", + border: "1px solid #D6DFF8", + "&.MuiAccordionSummary-root .MuiAccordionSummary-content": + { + justifyContent: "space-between", + flexWrap: "wrap", + }, + }} + > + + {type} + + + {getBotType(type).shouldBlock ? ( + + ) : ( + + )} + + + + { + e.stopPropagation(); + bulkToggle(robots, e.target.checked); + }} + inputProps={{ "aria-label": "controlled" }} + /> + } + label={Block all} + onClick={(e) => e.stopPropagation()} + /> + + + + {robots.map((robot) => ( + + toggleBot(robot)} + sx={{ + color: "primary.main", + "&.Mui-checked": { + color: "primary.main", + }, + }} + /> + } + label={ + + {robot.name} + + + + + + + } + sx={{ + marginLeft: "0 !important", + }} + /> + + ))} + + + + ))} + + + } + + + + ); +} diff --git a/apps/roboshield/src/components/CommonBots/index.ts b/apps/roboshield/src/components/CommonBots/index.ts new file mode 100644 index 000000000..035dc302b --- /dev/null +++ b/apps/roboshield/src/components/CommonBots/index.ts @@ -0,0 +1,3 @@ +import CommonBots from "./CommonBots"; + +export default CommonBots; diff --git a/apps/roboshield/src/components/CommonSettings/CommonSettings.tsx b/apps/roboshield/src/components/CommonSettings/CommonSettings.tsx new file mode 100644 index 000000000..da22c9f11 --- /dev/null +++ b/apps/roboshield/src/components/CommonSettings/CommonSettings.tsx @@ -0,0 +1,193 @@ +import InfoIcon from "@mui/icons-material/Info"; +import { + Box, + Stack, + InputLabel, + TextareaAutosize, + SelectChangeEvent, + Select, + MenuItem, +} from "@mui/material"; +import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; +import { ChangeEvent, useState } from "react"; + +import StepperNav from "@/roboshield/components/StepperNav"; +import { useGlobalState } from "@/roboshield/context/GlobalContext"; +import { platforms } from "@/roboshield/lib/config"; +import { StepComponent } from "@/roboshield/types/stepComponent"; + +export default function CommonSettings({ + handleNext, + handleBack, + lastStep, +}: StepComponent) { + const { state } = useGlobalState(); + + const [disallowedPaths, setDisallowedPaths] = useState( + state.disallowedPaths, + ); + const [allowedPaths, setAllowedPaths] = useState( + state.allowedPaths, + ); + const [platform, setPlatform] = useState(state.platform); + + const handleDisallowedPathsChange = ( + value: ChangeEvent, + ) => { + const data = value.target.value; + setDisallowedPaths(data.split("\n")); + }; + + const handleAllowedPathsChange = ( + value: ChangeEvent, + ) => { + const data = value.target.value; + setAllowedPaths(data.split("\n")); + }; + const handlePlatformChange = (event: SelectChangeEvent) => { + const selectedPlatform = platforms.find( + (platform) => platform.name === event.target.value, + ); + if (selectedPlatform) { + setDisallowedPaths(selectedPlatform?.disallowedPaths); + setAllowedPaths(selectedPlatform?.allowedPaths); + } + setPlatform(event.target.value as string); + }; + + const next = () => { + handleNext({ + disallowedPaths, + allowedPaths, + platform, + }); + }; + + return ( + <> + + {/* Platform */} + + + Select platform + + + + + + + + + {/* Disallowed paths */} + + + Disallowed paths + + + + + + + + + + + Allowed paths + + + + + + + + + + + + ); +} diff --git a/apps/roboshield/src/components/CommonSettings/index.ts b/apps/roboshield/src/components/CommonSettings/index.ts new file mode 100644 index 000000000..d0740a30a --- /dev/null +++ b/apps/roboshield/src/components/CommonSettings/index.ts @@ -0,0 +1,3 @@ +import CommonSettings from "./CommonSettings"; + +export default CommonSettings; diff --git a/apps/roboshield/src/components/Delays/Delays.tsx b/apps/roboshield/src/components/Delays/Delays.tsx new file mode 100644 index 000000000..a8ea12162 --- /dev/null +++ b/apps/roboshield/src/components/Delays/Delays.tsx @@ -0,0 +1,162 @@ +import InfoIcon from "@mui/icons-material/Info"; +import { Box, IconButton, InputLabel, Stack, Tooltip } from "@mui/material"; +import { useState } from "react"; + +import StepperNav from "@/roboshield/components/StepperNav"; +import TimePicker from "@/roboshield/components/TimePicker"; + +import Input from "@/roboshield/components/Input"; +import { useGlobalState } from "@/roboshield/context/GlobalContext"; +import { StepComponent } from "@/roboshield/types/stepComponent"; + +export default function Delays({ + handleNext, + handleBack, + lastStep, +}: StepComponent) { + const { state } = useGlobalState(); + + const [crawlDelay, setCrawlDelay] = useState(state.crawlDelay); + const [cachedDelay, setCachedDelay] = useState(state.cachedDelay); + const [visitTimeFrom, setVisitTime] = useState(state.visitTimeFrom); + const [visitTimeTo, setVisitTimeTo] = useState(state.visitTimeTo); + + const handleCrawlDelayChange = (value: string) => { + setCrawlDelay(parseInt(value)); + }; + + const handleCacheDelayChange = (value: string) => { + setCachedDelay(parseInt(value)); + }; + + const handleVisitTimeChange = (value: Date | null) => { + if (value === null) { + return; + } + setVisitTime(value); + }; + + const handleVisitTimeToChange = (value: Date | null) => { + if (value === null) { + return; + } + setVisitTimeTo(value); + }; + + const next = () => { + handleNext({ + crawlDelay, + cachedDelay, + visitTimeFrom, + visitTimeTo, + }); + }; + return ( + <> + + + + + Crawl delay + + + + + + + + + + + Cache delay + + + + + + + + + + + + Visit time + + + + + + + + + + + + + + + ); +} diff --git a/apps/roboshield/src/components/Delays/index.ts b/apps/roboshield/src/components/Delays/index.ts new file mode 100644 index 000000000..78a74817d --- /dev/null +++ b/apps/roboshield/src/components/Delays/index.ts @@ -0,0 +1,3 @@ +import Delays from "./Delays"; + +export default Delays; diff --git a/apps/roboshield/src/components/DesktopNavBar/DesktopNavBar.tsx b/apps/roboshield/src/components/DesktopNavBar/DesktopNavBar.tsx new file mode 100644 index 000000000..7d2235ebb --- /dev/null +++ b/apps/roboshield/src/components/DesktopNavBar/DesktopNavBar.tsx @@ -0,0 +1,58 @@ +import { Box, Grid, Grid2Props } from "@mui/material"; +import React, { ForwardedRef } from "react"; + +import NavBarNavList from "@/roboshield/components/NavBarNavList"; +import NextImageButton from "@/roboshield/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 = React.forwardRef(function DesktopNavBar( + props: Props, + ref: ForwardedRef, +) { + const { logo, menus, socialLinks, sx } = props; + + return ( + + + + + + + + + + + ); +}); + +export default DesktopNavBar; diff --git a/apps/roboshield/src/components/DesktopNavBar/index.ts b/apps/roboshield/src/components/DesktopNavBar/index.ts new file mode 100644 index 000000000..3919164ee --- /dev/null +++ b/apps/roboshield/src/components/DesktopNavBar/index.ts @@ -0,0 +1,3 @@ +import DesktopNavBar from "./DesktopNavBar"; + +export default DesktopNavBar; diff --git a/apps/roboshield/src/components/ExistingRobots/ExistingRobots.tsx b/apps/roboshield/src/components/ExistingRobots/ExistingRobots.tsx new file mode 100644 index 000000000..0eec6c116 --- /dev/null +++ b/apps/roboshield/src/components/ExistingRobots/ExistingRobots.tsx @@ -0,0 +1,152 @@ +import { Box, Button, Snackbar, Stack, Typography } from "@mui/material"; +import Alert from "@mui/material/Alert"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import FormGroup from "@mui/material/FormGroup"; +import { useState } from "react"; + +import Input from "@/roboshield/components/Input"; +import StepperNav from "@/roboshield/components/StepperNav"; +import { useGlobalState } from "@/roboshield/context/GlobalContext"; +import { StepComponent } from "@/roboshield/types/stepComponent"; +import { validateUrl } from "@/roboshield/utils/urls"; + +export default function ExistingRobots({ + handleNext, + handleBack, + lastStep, +}: StepComponent) { + const { state } = useGlobalState(); + const [url, setUrl] = useState(state.url); + const [isValid, setIsValid] = useState(false); + const [showURLError, setShowURLError] = useState(false); + const [shouldFetch, setShouldFetch] = useState(state.shouldFetch); + const [robots, setRobots] = useState(state.robots); + const [allowNextStep, setAllowNextStep] = useState(false); + const [robotsError, setRobotsError] = useState(false); + + const onInputChange = (e: string) => { + const isValid = validateUrl(e); + if (isValid) { + setIsValid(true); + setUrl(e); + } else { + setIsValid(false); + } + }; + + const fetchRobots = async () => { + const res = await fetch("/api/fetch_robots", { + headers: { + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ url }), + }); + const data = await res.json(); + return data; + }; + + const fetchData = async () => { + if (!isValid) { + setShowURLError(true); + return; + } else { + setShowURLError(false); + const { robots, error } = await fetchRobots(); + if (error) { + setRobotsError(true); + return; + } + setRobots(robots); + setAllowNextStep(true); + } + }; + + const next = () => { + handleNext({ + url, + shouldFetch, + ...robots, + }); + }; + + return ( + <> + + + setShouldFetch(e.target.checked)} + name="fetch" + sx={{ + color: "primary.main", + "&.Mui-checked": { + color: "primary.main", + }, + }} + /> + } + label={Fetch existing robots.txt} + /> + + + + + + {showURLError && ( + + Please enter a valid URL. A valid URL should start with http:// or + https:// + + )} + + + + setRobotsError(false)} + message="Error fetching robots.txt file. Please try again." + /> + + ); +} diff --git a/apps/roboshield/src/components/ExistingRobots/index.ts b/apps/roboshield/src/components/ExistingRobots/index.ts new file mode 100644 index 000000000..a3feb9a3b --- /dev/null +++ b/apps/roboshield/src/components/ExistingRobots/index.ts @@ -0,0 +1,3 @@ +import ExistingRobots from "./ExistingRobots"; + +export default ExistingRobots; diff --git a/apps/roboshield/src/components/Finish/Finish.tsx b/apps/roboshield/src/components/Finish/Finish.tsx new file mode 100644 index 000000000..0026a4655 --- /dev/null +++ b/apps/roboshield/src/components/Finish/Finish.tsx @@ -0,0 +1,102 @@ +import { Box, Snackbar } from "@mui/material"; +import { useEffect, useState } from "react"; + +import Code from "../Code"; +import StepperNav from "../StepperNav"; + +import { useGlobalState } from "@/roboshield/context/GlobalContext"; +import { generateRobots } from "@/roboshield/lib/robots"; +import { StepComponent } from "@/roboshield/types/stepComponent"; +import { downloadFile } from "@/roboshield/utils/file"; + +export default function Finish({ + handleReset, + handleBack, +}: StepComponent & { handleReset: () => void }) { + const { state } = useGlobalState(); + const [code, setCode] = useState(state.robots || ""); + const [showSnackbar, setShowSnackbar] = useState(false); + const [saved, setSaved] = useState(false); + + async function saveData() { + await fetch("/api/save_robots", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ data: state }), + }); + } + + const getCopyMetadata = () => { + const date = new Date().toISOString(); + const url = window.location.href; + return `${code}\n\n\n# Generated on: ${date}\n# URL: ${url}\n\n`; + }; + + const handleDownload = async () => { + const filename = "robots.txt"; + if (!saved) { + await saveData(); + setSaved(true); + } + await downloadFile(filename, getCopyMetadata()); + }; + + const handleCopy = async () => { + if (!saved) { + await saveData(); + setSaved(true); + } + navigator.clipboard.writeText(getCopyMetadata()); + setShowSnackbar(true); + }; + + const handleCodeChange = (newCode: string) => { + setCode(newCode); + }; + + useEffect(() => { + const generateRobotsFile = async () => { + const robots = await generateRobots(state); + setCode(robots); + }; + + generateRobotsFile(); + }, [state]); + + return ( + <> + + + + {}} + handleBack={handleBack} + isValid={true} + lastStep={true} + back={false} + /> + setShowSnackbar(false)} + message="Copied to clipboard" + /> + + ); +} diff --git a/apps/roboshield/src/components/Finish/index.ts b/apps/roboshield/src/components/Finish/index.ts new file mode 100644 index 000000000..3d956119c --- /dev/null +++ b/apps/roboshield/src/components/Finish/index.ts @@ -0,0 +1,3 @@ +import Finish from "./Finish"; + +export default Finish; diff --git a/apps/roboshield/src/components/Footer/Footer.tsx b/apps/roboshield/src/components/Footer/Footer.tsx new file mode 100644 index 000000000..9161cc03c --- /dev/null +++ b/apps/roboshield/src/components/Footer/Footer.tsx @@ -0,0 +1,172 @@ +import { Section } from "@commons-ui/core"; +import { Figure, Link } from "@commons-ui/next"; +import { Box, Grid, Typography } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +import FooterDescription from "./FooterDescription"; + +interface FooterProps { + logo: any; + partners: any[]; + description: string; +} + +const FooterRoot = styled(Box)( + ({ theme: { breakpoints, palette, typography } }) => ({ + backgroundColor: palette.common.black, + color: palette.text.secondary, + padding: `${typography.pxToRem(80)} 0`, + [breakpoints.up("md")]: { + padding: `${typography.pxToRem(110)} 0`, + }, + [breakpoints.up("lg")]: { + padding: `${typography.pxToRem(100)} 0`, + }, + }), +); + +export default function Footer({ logo, description, partners }: FooterProps) { + return ( + +
+ + + + + + + + + + + + + + In partnership with: + + + {partners.map((partner: any) => ( + + +
+ + + ))} + + + + + + + This project was insipred by a{" "} + + survey conducted{" "} + + by the Reutures Instititue in the Minority World + + + The Audit data used in this project was based on{" "} + + Civic Signals{" "} + + MediaData DB + + + + +
+
+ ); +} diff --git a/apps/roboshield/src/components/Footer/FooterDescription.tsx b/apps/roboshield/src/components/Footer/FooterDescription.tsx new file mode 100644 index 000000000..b91d1d7eb --- /dev/null +++ b/apps/roboshield/src/components/Footer/FooterDescription.tsx @@ -0,0 +1,49 @@ +import { Figure, Link } from "@commons-ui/next"; +import { Stack, Typography } from "@mui/material"; +import React from "react"; + +interface FooterDescriptionProps { + description: any; + logo: any; + sx?: any; +} + +function FooterDescription({ description, logo, sx }: FooterDescriptionProps) { + if (!(logo || description)) { + return null; + } + return ( + + +
+ + + {description} + + + ); +} + +export default FooterDescription; diff --git a/apps/roboshield/src/components/Footer/index.ts b/apps/roboshield/src/components/Footer/index.ts new file mode 100644 index 000000000..a64cd38db --- /dev/null +++ b/apps/roboshield/src/components/Footer/index.ts @@ -0,0 +1,3 @@ +import Footer from "./Footer"; + +export default Footer; diff --git a/apps/roboshield/src/components/Hero/Hero.tsx b/apps/roboshield/src/components/Hero/Hero.tsx new file mode 100644 index 000000000..d361f57db --- /dev/null +++ b/apps/roboshield/src/components/Hero/Hero.tsx @@ -0,0 +1,115 @@ +import { Box } from "@mui/material"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; +import React from "react"; +import ReactRotatingText from "react-rotating-text"; + +interface props { + scrolRef: React.RefObject; +} + +const Hero = ({ scrolRef }: props) => { + return ( + + + + HOW IT WORKS + + + + + + Guard your{" "} + + + {" "} + against AI Bots + + + + Generate a robots.txt file tailored to the platform you use to publish + your content online and blocks AI bots + + + + + ); +}; + +export default Hero; diff --git a/apps/roboshield/src/components/Hero/index.ts b/apps/roboshield/src/components/Hero/index.ts new file mode 100644 index 000000000..4ca8fd8ad --- /dev/null +++ b/apps/roboshield/src/components/Hero/index.ts @@ -0,0 +1,3 @@ +import Hero from "./Hero"; + +export default Hero; diff --git a/apps/roboshield/src/components/Input/Input.tsx b/apps/roboshield/src/components/Input/Input.tsx new file mode 100644 index 000000000..c64ce9a6a --- /dev/null +++ b/apps/roboshield/src/components/Input/Input.tsx @@ -0,0 +1,40 @@ +import { TextField } from "@mui/material"; +import React, { useState } from "react"; + +import { useDebouncedValue } from "@/roboshield/utils/useDebounce"; + +interface InputProps { + initialValue?: string; + label?: string; + + onChange?: (value: string) => void; + placeholder?: string; + sx?: React.CSSProperties; + disabled?: boolean; +} + +const Input = React.forwardRef( + function Input(props, ref) { + const { onChange, initialValue = "", disabled = false, ...other } = props; + + const [value, setValue] = useState(initialValue); + + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + }; + + useDebouncedValue(value, 500, onChange); + + return ( + + ); + }, +); + +export default Input; diff --git a/apps/roboshield/src/components/Input/index.ts b/apps/roboshield/src/components/Input/index.ts new file mode 100644 index 000000000..523987d65 --- /dev/null +++ b/apps/roboshield/src/components/Input/index.ts @@ -0,0 +1,3 @@ +import Input from "./Input"; + +export default Input; diff --git a/apps/roboshield/src/components/MobileNavBar/MobileNavBar.tsx b/apps/roboshield/src/components/MobileNavBar/MobileNavBar.tsx new file mode 100644 index 000000000..f48c6291e --- /dev/null +++ b/apps/roboshield/src/components/MobileNavBar/MobileNavBar.tsx @@ -0,0 +1,141 @@ +import { + Dialog, + DialogContent, + Grid, + Grid2Props, + IconButton, + Slide, + SlideProps, + SvgIcon, +} from "@mui/material"; +import { styled } from "@mui/material/styles"; +import React, { ForwardedRef } from "react"; + +import menuIcon from "@/roboshield/assets/icons/menu-icon.svg"; +import CloseIcon from "@/roboshield/assets/icons/Type=x, Size=24, Color=CurrentColor.svg"; +import NavBarNavList from "@/roboshield/components/NavBarNavList"; +import NextImageButton from "@/roboshield/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 = React.forwardRef(function Transition( + { children, ...props }: SlideProps, + ref, +) { + return ( + + {children} + + ); +}); + +const MobileNavBar = React.forwardRef(function MobileNavBar( + props: 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/roboshield/src/components/MobileNavBar/index.ts b/apps/roboshield/src/components/MobileNavBar/index.ts new file mode 100644 index 000000000..b19184643 --- /dev/null +++ b/apps/roboshield/src/components/MobileNavBar/index.ts @@ -0,0 +1,3 @@ +import MobileNavBar from "./MobileNavBar"; + +export default MobileNavBar; diff --git a/apps/roboshield/src/components/NavBar/NavBar.tsx b/apps/roboshield/src/components/NavBar/NavBar.tsx new file mode 100644 index 000000000..6c7755ea0 --- /dev/null +++ b/apps/roboshield/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 "@/roboshield/components/DesktopNavBar"; +import MobileNavBar from "@/roboshield/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/roboshield/src/components/NavBar/index.ts b/apps/roboshield/src/components/NavBar/index.ts new file mode 100644 index 000000000..085b6b525 --- /dev/null +++ b/apps/roboshield/src/components/NavBar/index.ts @@ -0,0 +1,3 @@ +import NavBar from "./NavBar"; + +export default NavBar; diff --git a/apps/roboshield/src/components/NavBarNavList/NavBarNavList.tsx b/apps/roboshield/src/components/NavBarNavList/NavBarNavList.tsx new file mode 100644 index 000000000..604459296 --- /dev/null +++ b/apps/roboshield/src/components/NavBarNavList/NavBarNavList.tsx @@ -0,0 +1,100 @@ +import { NavList } from "@commons-ui/core"; +import { Link } from "@commons-ui/next"; +import { LinkProps, SvgIcon } from "@mui/material"; +import React, { ElementType, FC } from "react"; + +import GitHubIcon from "@/roboshield/assets/icons/Type=github, Size=24, Color=CurrentColor.svg"; +import NavListItem from "@/roboshield/components/NavListItem"; + +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; + + 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/roboshield/src/components/NavBarNavList/index.ts b/apps/roboshield/src/components/NavBarNavList/index.ts new file mode 100644 index 000000000..f261c9be0 --- /dev/null +++ b/apps/roboshield/src/components/NavBarNavList/index.ts @@ -0,0 +1,3 @@ +import NavBarNavList from "./NavBarNavList"; + +export default NavBarNavList; diff --git a/apps/roboshield/src/components/NavListItem/NavListItem.tsx b/apps/roboshield/src/components/NavListItem/NavListItem.tsx new file mode 100644 index 000000000..5cde403fc --- /dev/null +++ b/apps/roboshield/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/roboshield/src/components/NavListItem/index.ts b/apps/roboshield/src/components/NavListItem/index.ts new file mode 100644 index 000000000..abc33a899 --- /dev/null +++ b/apps/roboshield/src/components/NavListItem/index.ts @@ -0,0 +1,3 @@ +import NavListItem from "./NavListItem"; + +export default NavListItem; diff --git a/apps/roboshield/src/components/NextImageButton/NextImageButton.tsx b/apps/roboshield/src/components/NextImageButton/NextImageButton.tsx new file mode 100644 index 000000000..e118ada07 --- /dev/null +++ b/apps/roboshield/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/roboshield/src/components/NextImageButton/index.ts b/apps/roboshield/src/components/NextImageButton/index.ts new file mode 100644 index 000000000..9e6d32a68 --- /dev/null +++ b/apps/roboshield/src/components/NextImageButton/index.ts @@ -0,0 +1,3 @@ +import NextImageButton from "./NextImageButton"; + +export default NextImageButton; diff --git a/apps/roboshield/src/components/Page/Page.tsx b/apps/roboshield/src/components/Page/Page.tsx new file mode 100644 index 000000000..b78623743 --- /dev/null +++ b/apps/roboshield/src/components/Page/Page.tsx @@ -0,0 +1,48 @@ +import React from "react"; + +import Footer from "../Footer"; + +import NavBar from "@/roboshield/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 Footer { + logo: any; + description: string; + partners: any[]; +} + +interface Props { + children?: React.ReactNode; + navbar?: Navbar; + footer?: Footer; +} +function Page({ children, navbar, footer }: Props) { + return ( + <> + {navbar ? : null} + {children ?
{children}
: null} + {footer ?