diff --git a/apps/partners/.eslintrc.json b/apps/partners/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/apps/partners/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/apps/partners/.gitignore b/apps/partners/.gitignore new file mode 100644 index 00000000..fd3dbb57 --- /dev/null +++ b/apps/partners/.gitignore @@ -0,0 +1,36 @@ +# 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 diff --git a/apps/partners/README.md b/apps/partners/README.md new file mode 100644 index 00000000..c4033664 --- /dev/null +++ b/apps/partners/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/partners/next.config.mjs b/apps/partners/next.config.mjs new file mode 100644 index 00000000..f46c0848 --- /dev/null +++ b/apps/partners/next.config.mjs @@ -0,0 +1,17 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ["@umamin/ui", "@umamin/db", "@umamin/gql"], + compiler: { + removeConsole: process.env.NODE_ENV === "production", + }, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "lh3.googleusercontent.com", + }, + ], + }, +}; + +export default nextConfig; diff --git a/apps/partners/package.json b/apps/partners/package.json new file mode 100644 index 00000000..44a5d8fe --- /dev/null +++ b/apps/partners/package.json @@ -0,0 +1,58 @@ +{ + "name": "partners", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@graphql-yoga/plugin-csrf-prevention": "^3.6.2", + "@graphql-yoga/plugin-disable-introspection": "^2.6.2", + "@graphql-yoga/plugin-persisted-operations": "^3.6.2", + "@graphql-yoga/plugin-response-cache": "^3.8.2", + "@lucia-auth/adapter-drizzle": "^1.0.7", + "@urql/core": "^5.0.5", + "@urql/exchange-graphcache": "^7.1.1", + "@urql/exchange-persisted": "^4.3.0", + "@urql/next": "^1.1.1", + "date-fns": "^3.6.0", + "geist": "^1.3.1", + "arctic": "^1.9.2", + "gql.tada": "^1.8.5", + "graphql": "^16.9.0", + "graphql-yoga": "^5.6.2", + "lucia": "^3.2.0", + "lucide-react": "^0.407.0", + "modern-screenshot": "^4.4.39", + "react-intersection-observer": "^9.10.2", + "urql": "^4.1.0", + "zod": "^3.22.4", + "sonner": "^1.5.0", + "nanoid": "^5.0.7", + "nextjs-toploader": "^1.6.12", + "@whatwg-node/server": "^0.9.46", + "@umamin/db": "workspace:*", + "@umamin/gql": "workspace:*", + "@umamin/ui": "workspace:*", + "react": "^18", + "react-dom": "^18", + "next": "14.2.5" + }, + "devDependencies": { + "@0no-co/graphqlsp": "^1.12.12", + "@umamin/eslint-config": "workspace:*", + "@umamin/tsconfig": "workspace:*", + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "eslint": "^8", + "eslint-config-next": "14.2.5" + } +} diff --git a/apps/partners/postcss.config.cjs b/apps/partners/postcss.config.cjs new file mode 100644 index 00000000..a4917b21 --- /dev/null +++ b/apps/partners/postcss.config.cjs @@ -0,0 +1 @@ +module.exports = require("@umamin/ui/postcss.config"); diff --git a/apps/partners/public/next.svg b/apps/partners/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/apps/partners/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/partners/public/vercel.svg b/apps/partners/public/vercel.svg new file mode 100644 index 00000000..d2f84222 --- /dev/null +++ b/apps/partners/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/partners/src/app/components/grid-pattern.tsx b/apps/partners/src/app/components/grid-pattern.tsx new file mode 100644 index 00000000..adff2a44 --- /dev/null +++ b/apps/partners/src/app/components/grid-pattern.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { cn } from "@umamin/ui/lib/utils"; +import { useId } from "react"; + +interface GridPatternProps { + width?: any; + height?: any; + x?: any; + y?: any; + squares?: Array<[x: number, y: number]>; + strokeDasharray?: any; + className?: string; + [key: string]: any; +} + +export function GridPattern({ + width = 40, + height = 40, + x = -1, + y = -1, + strokeDasharray = 0, + squares, + className, + ...props +}: GridPatternProps) { + const id = useId(); + + return ( + + ); +} + +export default GridPattern; diff --git a/apps/partners/src/app/components/particles.tsx b/apps/partners/src/app/components/particles.tsx new file mode 100644 index 00000000..a51f34d8 --- /dev/null +++ b/apps/partners/src/app/components/particles.tsx @@ -0,0 +1,270 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; + +interface MousePosition { + x: number; + y: number; +} + +function MousePosition(): MousePosition { + const [mousePosition, setMousePosition] = useState({ + x: 0, + y: 0, + }); + + useEffect(() => { + const handleMouseMove = (event: MouseEvent) => { + setMousePosition({ x: event.clientX, y: event.clientY }); + }; + + window.addEventListener("mousemove", handleMouseMove); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + }; + }, []); + + return mousePosition; +} + +interface ParticlesProps { + className?: string; + quantity?: number; + staticity?: number; + ease?: number; + size?: number; + refresh?: boolean; + color?: string; + vx?: number; + vy?: number; +} +function hexToRgb(hex: string): number[] { + hex = hex.replace("#", ""); + const hexInt = parseInt(hex, 16); + const red = (hexInt >> 16) & 255; + const green = (hexInt >> 8) & 255; + const blue = hexInt & 255; + return [red, green, blue]; +} + +const Particles: React.FC = ({ + className = "", + quantity = 100, + staticity = 50, + ease = 50, + size = 0.4, + refresh = false, + color = "#ffffff", + vx = 0, + vy = 0, +}) => { + const canvasRef = useRef(null); + const canvasContainerRef = useRef(null); + const context = useRef(null); + const circles = useRef([]); + const mousePosition = MousePosition(); + const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); + const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1; + + useEffect(() => { + if (canvasRef.current) { + context.current = canvasRef.current.getContext("2d"); + } + initCanvas(); + animate(); + window.addEventListener("resize", initCanvas); + + return () => { + window.removeEventListener("resize", initCanvas); + }; + }, [color]); + + useEffect(() => { + onMouseMove(); + }, [mousePosition.x, mousePosition.y]); + + useEffect(() => { + initCanvas(); + }, [refresh]); + + const initCanvas = () => { + resizeCanvas(); + drawParticles(); + }; + + const onMouseMove = () => { + if (canvasRef.current) { + const rect = canvasRef.current.getBoundingClientRect(); + const { w, h } = canvasSize.current; + const x = mousePosition.x - rect.left - w / 2; + const y = mousePosition.y - rect.top - h / 2; + const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2; + if (inside) { + mouse.current.x = x; + mouse.current.y = y; + } + } + }; + + type Circle = { + x: number; + y: number; + translateX: number; + translateY: number; + size: number; + alpha: number; + targetAlpha: number; + dx: number; + dy: number; + magnetism: number; + }; + + const resizeCanvas = () => { + if (canvasContainerRef.current && canvasRef.current && context.current) { + circles.current.length = 0; + canvasSize.current.w = canvasContainerRef.current.offsetWidth; + canvasSize.current.h = canvasContainerRef.current.offsetHeight; + canvasRef.current.width = canvasSize.current.w * dpr; + canvasRef.current.height = canvasSize.current.h * dpr; + canvasRef.current.style.width = `${canvasSize.current.w}px`; + canvasRef.current.style.height = `${canvasSize.current.h}px`; + context.current.scale(dpr, dpr); + } + }; + + const circleParams = (): Circle => { + const x = Math.floor(Math.random() * canvasSize.current.w); + const y = Math.floor(Math.random() * canvasSize.current.h); + const translateX = 0; + const translateY = 0; + const pSize = Math.floor(Math.random() * 2) + size; + const alpha = 0; + const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); + const dx = (Math.random() - 0.5) * 0.1; + const dy = (Math.random() - 0.5) * 0.1; + const magnetism = 0.1 + Math.random() * 4; + return { + x, + y, + translateX, + translateY, + size: pSize, + alpha, + targetAlpha, + dx, + dy, + magnetism, + }; + }; + + const rgb = hexToRgb(color); + + const drawCircle = (circle: Circle, update = false) => { + if (context.current) { + const { x, y, translateX, translateY, size, alpha } = circle; + context.current.translate(translateX, translateY); + context.current.beginPath(); + context.current.arc(x, y, size, 0, 2 * Math.PI); + context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`; + context.current.fill(); + context.current.setTransform(dpr, 0, 0, dpr, 0, 0); + + if (!update) { + circles.current.push(circle); + } + } + }; + + const clearContext = () => { + if (context.current) { + context.current.clearRect( + 0, + 0, + canvasSize.current.w, + canvasSize.current.h + ); + } + }; + + const drawParticles = () => { + clearContext(); + const particleCount = quantity; + for (let i = 0; i < particleCount; i++) { + const circle = circleParams(); + drawCircle(circle); + } + }; + + const remapValue = ( + value: number, + start1: number, + end1: number, + start2: number, + end2: number + ): number => { + const remapped = + ((value - start1) * (end2 - start2)) / (end1 - start1) + start2; + return remapped > 0 ? remapped : 0; + }; + + const animate = () => { + clearContext(); + circles.current.forEach((circle: Circle, i: number) => { + // Handle the alpha value + const edge = [ + circle.x + circle.translateX - circle.size, // distance from left edge + canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge + circle.y + circle.translateY - circle.size, // distance from top edge + canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge + ]; + const closestEdge = edge.reduce((a, b) => Math.min(a, b)); + const remapClosestEdge = parseFloat( + remapValue(closestEdge, 0, 20, 0, 1).toFixed(2) + ); + if (remapClosestEdge > 1) { + circle.alpha += 0.02; + if (circle.alpha > circle.targetAlpha) { + circle.alpha = circle.targetAlpha; + } + } else { + circle.alpha = circle.targetAlpha * remapClosestEdge; + } + circle.x += circle.dx + vx; + circle.y += circle.dy + vy; + circle.translateX += + (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / + ease; + circle.translateY += + (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / + ease; + + drawCircle(circle, true); + + // circle gets out of the canvas + if ( + circle.x < -circle.size || + circle.x > canvasSize.current.w + circle.size || + circle.y < -circle.size || + circle.y > canvasSize.current.h + circle.size + ) { + // remove the circle from the array + circles.current.splice(i, 1); + // create a new circle + const newCircle = circleParams(); + drawCircle(newCircle); + // update the circle position + } + }); + window.requestAnimationFrame(animate); + }; + + return ( + + ); +}; + +export default Particles; diff --git a/apps/partners/src/app/favicon.ico b/apps/partners/src/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/apps/partners/src/app/favicon.ico differ diff --git a/apps/partners/src/app/layout.tsx b/apps/partners/src/app/layout.tsx new file mode 100644 index 00000000..b8ec7ac0 --- /dev/null +++ b/apps/partners/src/app/layout.tsx @@ -0,0 +1,104 @@ +import { Toaster } from "sonner"; +import Script from "next/script"; +import { GeistSans } from "geist/font/sans"; +import NextTopLoader from "nextjs-toploader"; +import type { Metadata, Viewport } from "next"; +import { ThemeProvider } from "@umamin/ui/components/theme-provider"; +import "@umamin/ui/globals.css"; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + themeColor: "black", +}; + +export const metadata: Metadata = { + metadataBase: new URL("https://partners.umamin.link"), + alternates: { + canonical: "/", + }, + title: "Umamin Partners — Anonymity at Scale", + authors: [{ name: "Omsimos Collective" }], + description: + "Umamin Partners provides powerful tools that are Ideal for businesses, organizations, or individuals dealing with large volumes of anonymous feedback, surveys, or communications. It combines the security and privacy of the core platform with powerful management tools to optimize workflow.", + keywords: [ + "anonymous messaging", + "open-source platform", + "encrypted messages", + "privacy", + "anonymity", + ], + openGraph: { + type: "website", + siteName: "Umamin Partners", + url: "https://social.umamin.link", + title: "Umamin Partners — Anonymity at Scale", + description: + "Umamin Partners provides powerful tools that are Ideal for businesses, organizations, or individuals dealing with large volumes of anonymous feedback, surveys, or communications. It combines the security and privacy of the core platform with powerful management tools to optimize workflow.", + }, + twitter: { + card: "summary_large_image", + title: "Umamin Partners — Anonymity at Scale", + description: + "Umamin Partners provides powerful tools that are Ideal for businesses, organizations, or individuals dealing with large volumes of anonymous feedback, surveys, or communications. It combines the security and privacy of the core platform with powerful management tools to optimize workflow.", + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + noimageindex: false, + "max-video-preview": -1, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + {children} + + + + {process.env.NODE_ENV === "production" && ( +