diff --git a/components/navigation/footer.js b/components/navigation/footer.js index 7e0f38b0d..eaef45416 100644 --- a/components/navigation/footer.js +++ b/components/navigation/footer.js @@ -2,7 +2,7 @@ import Link from "next/link"; import styles from "./footer.module.css"; -const Footer = () => { +const Footer = ({ setIsTelemetryModalVisible }) => { return ( ); diff --git a/components/utilities/breadCrumbs.js b/components/utilities/breadCrumbs.js index 221750d82..ba36aedf9 100644 --- a/components/utilities/breadCrumbs.js +++ b/components/utilities/breadCrumbs.js @@ -65,7 +65,7 @@ const BreadCrumbs = ({ slug, menu }) => { } // Then, we add a couple pages that don't need breadcrumbs, such as /menu, /index, etc. - filesToExclude.push("index", "gdpr-banner", "menu"); + filesToExclude.push("index", "gdpr-banner", "menu", "cookie-settings"); // Now, we throw the error if any page that's not on the filesToExclude array is missing in menu.md if (path.length === 0 && !filesToExclude.includes(slug[0])) { diff --git a/components/utilities/cookieSettingsModal.js b/components/utilities/cookieSettingsModal.js new file mode 100644 index 000000000..0005123d1 --- /dev/null +++ b/components/utilities/cookieSettingsModal.js @@ -0,0 +1,73 @@ +import classNames from "classnames"; +import { MDXRemote } from "next-mdx-remote"; + +import styles from "./cookieSettingsModal.module.css"; + +export default function CookieSettingsModal({ + setIsTelemetryModalVisible, + declineTelemetryAndCloseBanner, + allowTelemetryAndCloseBanner, + content, +}) { + return ( +
+
+ + + + +
+
+ ); +} diff --git a/components/utilities/cookieSettingsModal.module.css b/components/utilities/cookieSettingsModal.module.css new file mode 100644 index 000000000..825ba029a --- /dev/null +++ b/components/utilities/cookieSettingsModal.module.css @@ -0,0 +1,167 @@ +.Container > div { + @apply text-gray-90 grid grid-cols-1 gap-4 text-base leading-loose; +} + +.Container h1, +.Container h2, +.Container h3, +.Container h4, +.Container h5, +.Container h6 { + @apply scroll-mt-0 md:scroll-mt-32; +} + +.Container h2, +.Container h3, +.Container h4, +.Container h5 { + @apply mt-4 text-lg font-bold text-gray-90; +} + +.Container h2 { + @apply text-xl; +} + +.Container h3 { + @apply text-2xl; +} + +.Container h2:first-child, +.Container h3:first-child, +.Container h4:first-child, +.Container h5:first-child { + @apply mt-0; +} + +.Container p { + @apply text-gray-90; +} + +.Container a { + @apply underline; +} + +.Container a:hover { + @apply opacity-80; +} + +.Container ol { + @apply list-decimal pl-8 grid grid-cols-1 gap-4; +} + +.Container section ol { + @apply list-none pl-0 gap-0 mb-5; +} + +.Container ul { + @apply list-disc pl-8 grid grid-cols-1 gap-4; +} + +.Container li { + @apply pl-2; +} + +.Container blockquote { + @apply p-6 md:p-8 lg:p-16 + -mx-6 md:-mx-8 lg:-mx-16 + my-0 md:my-4 lg:my-12 + grid grid-cols-1 gap-4 + bg-lightBlue-10 + text-gray-80 + rounded-lg; +} + +.PrivacyContainer blockquote ~ h4 { + @apply mt-12 md:mt-8 lg:mt-0 text-gray-90; +} + +.PrivacyContainer blockquote h2 { + @apply hidden; +} + +.Container blockquote h2 { + @apply text-gray-90 text-3xl; +} + +.Container hr { + @apply border-gray-40 border-t-0 border-l-0 border-r-0 border-b my-8; +} + +.Container address { + @apply whitespace-pre-line pl-4 border-l-2 border-gray-30 mt-4; +} + +.Container pre { + @apply bg-gray-10 p-4 rounded-md overflow-x-auto text-gray-90; +} + +.Container pre code { + @apply text-lg; +} + +.Container figure { + @apply flex flex-col items-start; +} + +.Container figure img { + @apply h-24 ml-2 mb-2; +} + +.Container figure figcaption { + @apply text-sm text-gray-70; +} + +/* For table in Privacy notice */ +.Container table { + @apply table-fixed my-8 w-full; +} + +.Container table thead tr th { + @apply px-4 py-3 text-left bg-gray-20 bg-opacity-50 border border-gray-30 text-gray-90 font-semibold text-lg leading-normal align-top; +} + +.Container table tbody tr td { + @apply px-4 py-3 border border-gray-30 text-gray-90 text-lg leading-normal align-top; +} + +.DeploymentTerms ul, +.DeploymentTerms ol { + @apply list-none; +} + +/* Specific classes for colors */ +.Container:global(.red) a { + @apply text-red-70; +} + +.Container:global(.orange) a { + @apply text-orange-70; +} + +.Container:global(.yellow) a { + @apply text-yellow-90; +} + +.Container:global(.green) a { + @apply text-green-70; +} + +.Container:global(.acqua) a { + @apply text-acqua-70; +} + +.Container:global(.lightBlue) a { + @apply text-lightBlue-70; +} + +.Container:global(.darkBlue) a { + @apply text-darkBlue-70; +} + +.Container:global(.indigo) a { + @apply text-indigo-70; +} + +.Container:global(.gray) a { + @apply text-gray-70; +} diff --git a/components/utilities/gdpr.js b/components/utilities/gdpr.js index daa2f8e19..5da6501d2 100644 --- a/components/utilities/gdpr.js +++ b/components/utilities/gdpr.js @@ -1,86 +1,157 @@ -// Global Imports -import React, { useState, useEffect, useCallback } from "react"; +import React, { useEffect } from "react"; import classNames from "classnames"; import { MDXRemote } from "next-mdx-remote"; -import { ReactComponent as CookieEmoji } from "../../images/icons/cookie.svg"; import styles from "./gdpr.module.css"; -const KEY = "InsertAnalyticsCode"; +const TELEMETRY_PREFERENCE = "InsertTelemetry"; +const TELEMETRY_PREFERENCE_DATE = "TelemetryDate"; -const GDPRBanner = (gdprData) => { - const title = gdprData.title; - const content = gdprData.content; +export function setTelemetryPreference(accepted) { + localStorage.setItem(TELEMETRY_PREFERENCE, JSON.stringify(accepted)); + localStorage.setItem(TELEMETRY_PREFERENCE_DATE, JSON.stringify(Date.now())); +} + +// Check if the stored date is > 6 months old +const isConsentStale = (timestamp) => { + const consent_date = new Date(parseInt(timestamp * 1000)); + const today = new Date(); + + const six_months_in_ms = + 1000 /*ms*/ * 60 /*s*/ * 60 /*min*/ * 24 /*h*/ * 30 /*days*/ * 6; /*months*/ + return today - consent_date > six_months_in_ms; +}; - // Start with default values - const [isVisible, setIsVisible] = useState(true); - const [insertAnalyticsCode, setInsertAnalyticsCode] = useState(false); +function getTelemetryPreference() { + // Returns true/false if user accepted/denied telemetry. + // Returns null if user never accepted/denied or consent is stale. - const AllowAndCloseBanner = useCallback(() => { - // Update state and set the decision into localStorage - setIsVisible(false); - setInsertAnalyticsCode(true); - localStorage.setItem(KEY, JSON.stringify(Date.now())); - }, [isVisible, insertAnalyticsCode]); + const telemetryPref = localStorage.getItem(TELEMETRY_PREFERENCE); + const consentIsStale = isConsentStale( + localStorage.getItem(TELEMETRY_PREFERENCE_DATE) + ); + + if (telemetryPref == null || consentIsStale) return null; - const DeclineAndCloseBanner = useCallback(() => { - // Update state and set the decision into localStorage - setIsVisible(false); - setInsertAnalyticsCode(false); - localStorage.setItem(KEY, false); - }, [isVisible, insertAnalyticsCode]); + return telemetryPref == "true"; +} +export default function GDPRBanner({ + content, + isTelemetryModalVisible, + setIsTelemetryModalVisible, + isTelemetryBannerVisible, + setIsTelemetryBannerVisible, + insertTelemetryCode, + setInsertTelemetryCode, + allowTelemetryAndCloseBanner, + declineTelemetryAndCloseBanner, +}) { useEffect(() => { - // Check if there's something in localStorage, and update the banner visibility based on that - const localStorageIsSetUp = localStorage.getItem(KEY) != null; - setIsVisible(!localStorageIsSetUp); + const pref = getTelemetryPreference(); + + switch (pref) { + case true: + setInsertTelemetryCode(true); + return; - if (localStorageIsSetUp) { - setInsertAnalyticsCode(localStorage.getItem(KEY) != "false"); + case false: + // This is already false at initialization, but it doesn't hurt to do the right thing + // here and make sure it's indeed false. + setInsertTelemetryCode(false); + return; + + case null: + localStorage.clear(); // Do we even need this line?? Seems dangerous to just clear the entire localStorage, as maybe some other library could be using it. + setIsTelemetryBannerVisible(true); + return; + + default: + console.log(`Unexpected telemetry preference: ${pref}`); + return; } }, []); useEffect(() => { - // TODO: Check if timestamp > 1 year, then show banner again before adding snippet - if (insertAnalyticsCode) { - insertAnalytics(); + if (insertTelemetryCode) { + insertTelemetry(); } - }, [insertAnalyticsCode]); + }, [insertTelemetryCode]); return ( <> - {isVisible && ( -
-
-
- -
-
-

{title}

+ {isTelemetryBannerVisible && ( +
+
+
-
- - -
+
+
+ + +
)} ); -}; +} -function insertAnalytics() { +function insertTelemetry() { (function () { var analytics = (window.analytics = window.analytics || []); if (!analytics.initialize) @@ -143,5 +214,3 @@ function insertAnalytics() { } })(); } - -export default GDPRBanner; diff --git a/components/utilities/gdpr.module.css b/components/utilities/gdpr.module.css index 21e0504fe..0a47774e9 100644 --- a/components/utilities/gdpr.module.css +++ b/components/utilities/gdpr.module.css @@ -1,63 +1,42 @@ .Container { - @apply fixed bottom-0 md:bottom-4 md:right-4 w-full z-30 md:max-w-2xl; + backdrop-filter: blur(10px); + background: #fffd; } -.BannerBackground { - @apply border-t border-t-gray-50 bg-gray-20 md:border md:border-gray-30 p-2 md:p-6 md:rounded-md flex flex-col md:flex-row; +.Markdown div { + @apply flex flex-col; } -.Title { - @apply mt-0 mb-2 pt-2 md:pt-0 font-sans font-bold text-2xl tracking-tight leading-tight; - @apply text-gray-90; +.Markdown h1 { + @apply text-gray-90 text-lg sm:text-xl font-bold mb-1; } -.ImageContainer { - @apply hidden md:block mb-4 md:mb-0 md:mt-2 mr-4; +.Markdown p { + @apply text-gray-90 text-base mb-0; } -.CtasContainer { - @apply flex mt-4 md:mt-6; +.Markdown p a { + @apply text-gray-90 underline hover:text-gray-70; } -.Button { - @apply rounded-md px-8 py-1 text-red-70 text-sm font-semibold tracking-normal block cursor-pointer mb-2 hover:opacity-90 hover:scale-105; +:global(.dark) .Container { + @apply bg-gray-100 border-gray-90; } -.DeclineButton { - @apply border border-solid bg-gray-20 border-gray-70 text-gray-90 mr-4 hover:opacity-80; +:global(.dark) .Markdown p, +:global(.dark) .Markdown p a { + @apply text-gray-20; } -.AllowButton { - @apply bg-gray-90 text-white hover:opacity-80; +:global(.dark) .Link, +:global(.dark) .Button { + @apply text-gray-20; } -/* MDX styles */ -.Container div p, -.Container div a { - @apply text-gray-90; +:global(.dark) .Button { + @apply border-gray-80; } -.Container div a { - @apply text-gray-70; -} - -.Container div p { - @apply m-0; -} - -/* Dark mode modifiers */ -:global(.dark) .BannerBackground { - @apply bg-gray-90 border-t-gray-80 md:border-gray-80; -} - -:global(.dark) .Container div p { +:global(.dark) .Markdown h1 { @apply text-white; } - -:global(.dark) .DeclineButton { - @apply bg-gray-90 text-white border-gray-80; -} - -:global(.dark) .AllowButton { - @apply bg-white text-gray-90; -} diff --git a/content/cookie-settings.md b/content/cookie-settings.md new file mode 100644 index 000000000..b422adc73 --- /dev/null +++ b/content/cookie-settings.md @@ -0,0 +1,21 @@ +--- +visible: false +--- + +### Cookie settings + +##### Strictly necessary cookies + +These cookies are necessary for the website to function and cannot be switched off. They are usually only set in response to actions made by you which amount to a request for services, such as setting your privacy preferences, logging in or filling in forms. + +##### Performance cookies + +These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site. They help us understand how visitors move around the site and which pages are most frequently visited. + +##### Functional cookies + +These cookies are used to record your choices and settings, maintain your preferences over time and recognize you when you return to our website. These cookies help us to personalize our content for you and remember your preferences. + +##### Targeting cookies + +These cookies may be deployed to our site by our advertising partners to build a profile of your interest and provide you with content that is relevant to you, including showing you relevant ads on other websites. diff --git a/content/gdpr-banner.md b/content/gdpr-banner.md index 3861ee19c..af0887a47 100644 --- a/content/gdpr-banner.md +++ b/content/gdpr-banner.md @@ -1,8 +1,8 @@ --- -enabled: false -title: We value your privacy. --- -We would like to use cookies to help us understand how users interact with this website. This is used, for example, to find out which parts of this site should be further improved. +# Hello there 👋 -More information can be found in our [Privacy Notice](https://www.streamlit.io/privacy-policy). +Thanks for stopping by! We use cookies to help us understand how you interact with our website. + +By clicking “Accept all”, you consent to our use of cookies. For more information, please see our [privacy policy](www.streamlit.io/privacy-policy). diff --git a/lib/api.js b/lib/api.js index 3066b6675..1fe5660bc 100644 --- a/lib/api.js +++ b/lib/api.js @@ -102,5 +102,13 @@ export async function getGDPRBanner() { const fileContents = fs.readFileSync(fullPath, "utf8"); const data = matter(fileContents); const markup = await serialize(data.content); - return { data: data.data, content: markup, title: data.data.title }; + return { data: data.data, content: markup }; +} + +export async function getCookieSettings() { + const fullPath = join(articleDirectory, `cookie-settings.md`); + const fileContents = fs.readFileSync(fullPath, "utf8"); + const data = matter(fileContents); + const markup = await serialize(data.content); + return { content: markup }; } diff --git a/pages/[...slug].js b/pages/[...slug].js index cce9a5d3b..fcf11e935 100644 --- a/pages/[...slug].js +++ b/pages/[...slug].js @@ -1,7 +1,7 @@ import fs from "fs"; import { join, basename } from "path"; import sortBy from "lodash/sortBy"; -import React from "react"; +import React, { useState, useCallback } from "react"; import Link from "next/link"; import Head from "next/head"; import { serialize } from "next-mdx-remote/serialize"; @@ -10,15 +10,20 @@ import { MDXRemote } from "next-mdx-remote"; import matter from "gray-matter"; import remarkUnwrapImages from "remark-unwrap-images"; import classNames from "classnames"; +import { useRouter } from "next/router"; // Site Components -import GDPRBanner from "../components/utilities/gdpr"; +import CookieSettingsModal from "../components/utilities/cookieSettingsModal"; +import GDPRBanner, { + setTelemetryPreference, +} from "../components/utilities/gdpr"; import { getArticleSlugs, getArticleSlugFromString, pythonDirectory, getMenu, getGDPRBanner, + getCookieSettings, } from "../lib/api"; import { getPreviousNextFromMenu } from "../lib/utils.js"; import useVersion from "../lib/useVersion.js"; @@ -75,8 +80,8 @@ export default function Article({ nextMenuItem, versionFromStaticLoad, versions, - paths, gdpr_data, + cookie_data, filename, }) { let versionWarning; @@ -84,6 +89,30 @@ export default function Article({ let suggestEditURL; const { sourceFile } = useAppContext(); + const [isTelemetryModalVisible, setIsTelemetryModalVisible] = useState(false); + const [isTelemetryBannerVisible, setIsTelemetryBannerVisible] = + useState(false); + const [insertTelemetryCode, setInsertTelemetryCode] = useState(false); + + const router = useRouter(); + + const allowTelemetryAndCloseBanner = useCallback(() => { + setIsTelemetryBannerVisible(false); + setIsTelemetryModalVisible(false); + setInsertTelemetryCode(true); + setTelemetryPreference(true); + }, [isTelemetryBannerVisible, insertTelemetryCode]); + + const declineTelemetryAndCloseBanner = useCallback(() => { + setIsTelemetryBannerVisible(false); + setIsTelemetryModalVisible(false); + setInsertTelemetryCode(false); + setTelemetryPreference(false); + + // If previous state was true, and now it's false, reload the page to remove telemetry JS + if (insertTelemetryCode) router.reload(); + }, [isTelemetryBannerVisible, insertTelemetryCode]); + suggestEditURL = Object.keys(streamlit).length > 0 && sourceFile ? sourceFile @@ -194,7 +223,25 @@ export default function Article({ }} > - + {isTelemetryModalVisible && ( + + )} +
@@ -261,7 +308,7 @@ export default function Article({
-