Skip to content

Commit

Permalink
Added missing gdpr cookie consent popup
Browse files Browse the repository at this point in the history
  • Loading branch information
bring-shrubbery committed Oct 9, 2024
1 parent ff2756b commit e548052
Show file tree
Hide file tree
Showing 9 changed files with 458 additions and 14 deletions.
2 changes: 2 additions & 0 deletions apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@tailwindcss/typography": "^0.5.15",
"@types/canvas-confetti": "^1.6.4",
"@vercel/analytics": "^1.3.1",
"@vercel/edge-config": "^1.3.0",
"allotment": "^1.20.2",
"analytics": "^0.8.14",
"autoprefixer": "10.4.20",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^0.2.1",
Expand Down
12 changes: 11 additions & 1 deletion apps/nextjs/src/app/(documents)/privacy-policy/page.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Privacy Policy for SVG to SwiftUI Converter by Quassum

_Last Updated: 09-12-2023_
_Last Updated: 09-10-2024_

## Introduction

Expand All @@ -14,6 +14,16 @@ This Privacy Policy outlines the practices of Quassum MB regarding the collectio
- **Analytics:** The application utilizes various analytics tools to collect anonymous traffic data. This data is used solely for improving the application and understanding user interaction patterns. The primary analytics tool used is Google Analytics. For more information on how Google Analytics collects and processes data, please refer to [How Google uses information from sites or apps that use our services](https://policies.google.com/technologies/partner-sites).
- **No Personal Data Collection:** The application does not collect, store, or process any personal information from its users. There is no functionality to create an account, and all usage is completely anonymous.

### Third-Party Data Collection

Since we're using third-party solutions for analytics, please refer to their respective privacy policies for more information:

- **Google Analytics:** [Google Privacy & Terms](https://policies.google.com/privacy)
- **Plausible Analytics:** [Plausible Privacy Policy](https://plausible.io/privacy)
- **Umami Analytics:** [Umami Privacy Policy](https://umami.is/privacy)
- **Figma:** [Figma Privacy Policy](https://www.figma.com/privacy/)
- **GitHub:** [GitHub Privacy Statement](https://docs.github.com/en/github/site-policy/github-privacy-statement)

### Figma Plugin

- **No Data Collection:** The Figma plugin version of our application does not collect any information, including traffic analytics or personal data. Figma themselves may collect some data, which is governed by their own privacy policy.
Expand Down
6 changes: 6 additions & 0 deletions apps/nextjs/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { PropsWithChildren, ReactNode } from "react";
import { Suspense } from "react";
import { Inter } from "next/font/google";
import Script from "next/script";
import { AnalyticsPopup } from "@/components/analytics-popup";
import { Toaster } from "@/components/ui/toaster";
import { cn } from "@/lib/utils";
import { Analytics } from "@vercel/analytics/react";
Expand Down Expand Up @@ -86,10 +87,15 @@ export default function RootLayout({
>
<Providers>
<Suspense>{announcement}</Suspense>

{children}
</Providers>
<Toaster />
<Analytics />

<Suspense>
<AnalyticsPopup />
</Suspense>
</body>

{/* umami.is Analytics Script (goes through vercel rewrite to analytics.quassum.com) */}
Expand Down
119 changes: 119 additions & 0 deletions apps/nextjs/src/components/analytics-popup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { CheckCircle2Icon, CheckIcon, LoaderCircleIcon } from "lucide-react";
import { usePlausible } from "next-plausible";
import useLocalStorage from "use-local-storage";

import { NeonGradientCard } from "./magic-ui/neon-gradient-card";
import { Button } from "./ui/button";

export const AnalyticsPopup = () => {
const plausible = usePlausible();
const [status, setStatus] = useState<
"idle" | "loading" | "success-initial" | "success-final" | "hidden"
>("idle");
const [analyticsAccepted, setAnalyticsAccepted] = useLocalStorage(
"analytics_accepted",
false,
);

const handleAccept = () => {
plausible("analytics-accepted");

setStatus("loading");
};

useEffect(() => {
if (status === "loading") {
setTimeout(() => {
setStatus("success-initial");
}, 500);
}

if (status === "success-initial") {
setTimeout(() => {
setStatus("success-final");
}, 10);
}

if (status === "success-final") {
setTimeout(() => {
setStatus("hidden");
}, 500);
}

if (status === "hidden") {
setTimeout(() => {
setAnalyticsAccepted(true);
}, 250);
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status]);

if (analyticsAccepted) return null;

return (
<NeonGradientCard
className={cn(
"fixed bottom-4 left-4 h-fit w-[400px] p-0 opacity-100 transition-opacity",
status === "hidden" && "opacity-0",
)}
>
<div className="space-y-2">
<h2 className="text-lg font-semibold">Before you continue 🍪</h2>
<p className="text-sm text-muted-foreground">
<b>We collect anonymous analytics</b>. You can see more information
about what we collect in our{" "}
<a
href="/privacy-policy"
className="text-blue-500 hover:text-blue-600"
>
privacy policy
</a>
. Your code never leaves your browser.
</p>

<div className="flex w-full items-center justify-between">
<div className="text-xs">
Before continuing, confirm that you're ok with that.{" "}
</div>

<div className="relative flex w-fit gap-2">
<Button onClick={handleAccept} className="gap-2">
<StatusIcon status={status} />
Accept
</Button>
</div>
</div>
</div>
</NeonGradientCard>
);
};

const StatusIcon = ({
status,
}: {
status: "idle" | "loading" | "success-initial" | "success-final" | "hidden";
}) => {
switch (status) {
case "idle":
return <CheckIcon size={20} className="-ml-1" />;
case "loading":
return <LoaderCircleIcon size={20} className="-ml-1 animate-spin" />;
case "success-initial":
case "success-final":
case "hidden":
return (
<CheckCircle2Icon
size={20}
className={cn(
"-ml-1 scale-0 text-green-400 transition-transform",
status === "success-final" && "scale-100",
)}
/>
);
}
};
131 changes: 131 additions & 0 deletions apps/nextjs/src/components/magic-ui/confetti.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import type { ButtonProps } from "@/components/ui/button";
import type {
GlobalOptions as ConfettiGlobalOptions,
CreateTypes as ConfettiInstance,
Options as ConfettiOptions,
} from "canvas-confetti";
import type { ReactNode } from "react";
import React, {
createContext,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
} from "react";
import { Button } from "@/components/ui/button";
import confetti from "canvas-confetti";

interface Api {
fire: (options?: ConfettiOptions) => void;
}

type Props = React.ComponentPropsWithRef<"canvas"> & {
options?: ConfettiOptions;
globalOptions?: ConfettiGlobalOptions;
manualstart?: boolean;
children?: ReactNode;
};

export type ConfettiRef = Api | null;

const ConfettiContext = createContext<Api>({} as Api);

const Confetti = forwardRef<ConfettiRef, Props>((props, ref) => {
const {
options,
globalOptions = { resize: true, useWorker: true },
manualstart = false,
children,
...rest
} = props;
const instanceRef = useRef<ConfettiInstance | null>(null); // confetti instance

const canvasRef = useCallback(
// https://react.dev/reference/react-dom/components/common#ref-callback
// https://reactjs.org/docs/refs-and-the-dom.html#callback-refs
(node: HTMLCanvasElement) => {
if (node !== null) {
// <canvas> is mounted => create the confetti instance
if (instanceRef.current) return; // if not already created
instanceRef.current = confetti.create(node, {
...globalOptions,
resize: true,
});
} else {
// <canvas> is unmounted => reset and destroy instanceRef
if (instanceRef.current) {
instanceRef.current.reset();
instanceRef.current = null;
}
}
},
[globalOptions],
);

// `fire` is a function that calls the instance() with `opts` merged with `options`
const fire = useCallback(
(opts = {}) => instanceRef.current?.({ ...options, ...opts }),
[options],
);

const api = useMemo(
() => ({
fire,
}),
[fire],
);

useImperativeHandle(ref, () => api, [api]);

useEffect(() => {
if (!manualstart) {
fire();
}
}, [manualstart, fire]);

return (
<ConfettiContext.Provider value={api}>
<canvas ref={canvasRef} {...rest} />
{children}
</ConfettiContext.Provider>
);
});

interface ConfettiButtonProps extends ButtonProps {
options?: ConfettiOptions &
ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
children?: React.ReactNode;
}

function ConfettiButton({ options, children, ...props }: ConfettiButtonProps) {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
confetti({
...options,
origin: {
x: x / window.innerWidth,
y: y / window.innerHeight,
},
});
};

return (
<Button onClick={handleClick} {...props}>
{children}
</Button>
);
}

Confetti.displayName = "Confetti";

export { Confetti, ConfettiButton };
Loading

0 comments on commit e548052

Please sign in to comment.