├── .env.example ├── .eslintrc.json ├── app ├── favicon.ico ├── layout.tsx ├── globals.css └── page.tsx ├── components ├── components.zip ├── elements │ ├── section-divider │ │ ├── index.ts │ │ ├── section-divider.tsx │ │ └── section-divider.module.scss │ ├── gradient-overlay │ │ ├── index.ts │ │ ├── gradient-overlay.types.ts │ │ ├── gradient-overlay.tsx │ │ └── gradient-overlay.module.scss │ └── section-wrapper │ │ ├── index.ts │ │ ├── section-wrapper.types.ts │ │ ├── section-wrapper.module.scss │ │ └── section-wrapper.tsx ├── features │ ├── descope-types.ts │ ├── logout-button.tsx │ ├── message.tsx │ └── one-tap-component.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── 3d-card.tsx │ └── background-gradient-animation.tsx ├── config ├── consts.config.ts └── fonts.config.ts ├── next.config.mjs ├── renovate.json ├── postcss.config.mjs ├── lib └── utils.ts ├── components.json ├── middleware.ts ├── .gitignore ├── public ├── right_arrow.svg ├── vercel.svg ├── next.svg ├── one_tap.svg ├── descope-logo.svg └── onetap_guru.svg ├── styles ├── _media.scss ├── _variables.scss ├── _theme.scss └── globals.scss ├── tsconfig.json ├── package.json ├── LICENSE ├── README.md └── tailwind.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_DESCOPE_PROJECT_ID= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/onetap/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /components/components.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/onetap/HEAD/components/components.zip -------------------------------------------------------------------------------- /config/consts.config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SECTION_THEME = 'dark'; 2 | export const LIGHT_THEME = 'light'; 3 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /components/elements/section-divider/index.ts: -------------------------------------------------------------------------------- 1 | import SectionDivider from './section-divider'; 2 | 3 | export default SectionDivider; 4 | -------------------------------------------------------------------------------- /components/elements/gradient-overlay/index.ts: -------------------------------------------------------------------------------- 1 | import GradientOverlay from './gradient-overlay'; 2 | 3 | export default GradientOverlay; 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>descope-sample-apps/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /components/elements/section-wrapper/index.ts: -------------------------------------------------------------------------------- 1 | import SectionWrapper from './section-wrapper'; 2 | 3 | export type * from './section-wrapper.types'; 4 | 5 | export default SectionWrapper; 6 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /components/elements/gradient-overlay/gradient-overlay.types.ts: -------------------------------------------------------------------------------- 1 | export interface GradientOverlayProps { 2 | gradientSrc?: any; 3 | className?: string; 4 | isDark?: boolean; 5 | priority?: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /components/features/descope-types.ts: -------------------------------------------------------------------------------- 1 | interface AuthenticationInfo { 2 | jwt: string; 3 | token: Token; 4 | cookies?: string[]; 5 | } 6 | interface Token { 7 | sub?: string; 8 | exp?: number; 9 | iss?: string; 10 | [claim: string]: unknown; 11 | } 12 | 13 | export type { AuthenticationInfo } -------------------------------------------------------------------------------- /components/elements/section-divider/section-divider.tsx: -------------------------------------------------------------------------------- 1 | import styles from './section-divider.module.scss'; 2 | 3 | const SectionDivider = ({ upsideDown }: { upsideDown?: boolean }) => ( 4 |
5 |
6 |
7 | ); 8 | 9 | export default SectionDivider; 10 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/features/logout-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useDescope } from "@descope/nextjs-sdk/client"; 4 | import { Button } from "../ui/button"; 5 | 6 | export default function LogoutButton() { 7 | 8 | const { logout } = useDescope(); 9 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | // src/middleware.ts 2 | import { authMiddleware } from '@descope/nextjs-sdk/server' 3 | 4 | export default authMiddleware({ 5 | // The Descope project ID to use for authentication 6 | // Defaults to process.env.DESCOPE_PROJECT_ID 7 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, 8 | publicRoutes: ["/"], 9 | redirectUrl: "/", 10 | }) 11 | 12 | export const config = { 13 | matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'] 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /public/right_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /styles/_media.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | // Breakpoints 4 | $breakpoints: ( 5 | xs: rem-convert(375px), 6 | sm: rem-convert(768px), 7 | sm-md: rem-convert(834px), 8 | md: rem-convert(992px), 9 | lg: rem-convert(1200px), 10 | xl: rem-convert(1366px), 11 | xxl: rem-convert(1500px), 12 | xxxl: rem-convert(1920px), 13 | xxxxl: rem-convert(2100px) 14 | ); 15 | 16 | @mixin media($device) { 17 | @media only screen and (min-width: #{map.get($breakpoints, $device)}) { 18 | @content; 19 | } 20 | } 21 | 22 | @mixin mediaMax($device) { 23 | @media only screen and (max-width: #{map.get($breakpoints, $device)}) { 24 | @content; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /components/elements/section-wrapper/section-wrapper.types.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties, ElementType, ReactNode, Ref } from 'react'; 2 | import type { ThreeElements } from '@react-three/fiber'; 3 | 4 | export interface SectionBaseProps { 5 | order: number; 6 | titlesLoop: string[]; 7 | id: string; 8 | } 9 | 10 | export interface SectionWrapperProps { 11 | as?: Exclude; 12 | className?: string; 13 | classNameWrapper?: string; 14 | styles?: CSSProperties; 15 | onClick?: () => void; 16 | childrenWrapper?: ReactNode; 17 | ref?: Ref; 18 | 19 | order: number; 20 | children: ReactNode | ReactNode[]; 21 | gradient?: any; 22 | gradientMobile?: any; 23 | gradientClassname?: string; 24 | } 25 | -------------------------------------------------------------------------------- /components/elements/gradient-overlay/gradient-overlay.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import clsx from 'clsx'; 3 | 4 | import type { GradientOverlayProps } from './gradient-overlay.types'; 5 | import styles from './gradient-overlay.module.scss'; 6 | 7 | const GradientOverlay = ({ 8 | gradientSrc, 9 | className, 10 | isDark, 11 | priority 12 | }: GradientOverlayProps) => { 13 | return ( 14 | <> 15 | {gradientSrc && ( 16 |
23 | 30 |
31 | )} 32 | {isDark &&
} 33 | 34 | ); 35 | }; 36 | 37 | export default GradientOverlay; 38 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { AuthProvider } from "@descope/nextjs-sdk"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "onetap.guru | Descope One Tap Demo", 11 | description: "Experience how adding Google One Tap to your app can give your users a semaless, secure login experience.", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onetap", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@descope/nextjs-sdk": "^0.3.14", 13 | "@descope/react-sdk": "^2.3.6", 14 | "@radix-ui/react-slot": "^1.1.0", 15 | "@react-three/fiber": "^8.17.14", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.1", 18 | "framer-motion": "^11.3.30", 19 | "lucide-react": "^0.435.0", 20 | "next": "14.2.35", 21 | "react": "^18", 22 | "react-dom": "^18", 23 | "sass": "^1.84.0", 24 | "tailwind-merge": "^2.5.2", 25 | "tailwindcss-animate": "^1.0.7" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^20", 29 | "@types/react": "^18", 30 | "@types/react-dom": "^18", 31 | "eslint": "^8", 32 | "eslint-config-next": "14.2.35", 33 | "postcss": "^8", 34 | "tailwindcss": "^3.4.1", 35 | "typescript": "^5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 descope-sample-apps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/fonts.config.ts: -------------------------------------------------------------------------------- 1 | import { Inter, Source_Code_Pro } from 'next/font/google'; 2 | import localFont from 'next/font/local'; 3 | 4 | export const roobertFont = localFont({ 5 | preload: false, 6 | src: [ 7 | { 8 | path: '../assets/fonts/roobert/Roobert-Medium.woff', 9 | weight: '500', 10 | style: 'normal' 11 | } 12 | ], 13 | fallback: [ 14 | '-apple-system', 15 | 'BlinkMacSystemFont', 16 | 'Segoe UI', 17 | 'Roboto', 18 | 'Oxygen', 19 | 'Ubuntu', 20 | 'Cantarell', 21 | 'Fira Sans', 22 | 'Droid Sans', 23 | 'Helvetica Neue', 24 | 'sans-serif' 25 | ], 26 | variable: '--font-primary' 27 | }); 28 | 29 | export const interFont = Inter({ 30 | preload: true, 31 | weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], 32 | fallback: [ 33 | '-apple-system', 34 | 'BlinkMacSystemFont', 35 | 'Segoe UI', 36 | 'Roboto', 37 | 'Oxygen', 38 | 'Ubuntu', 39 | 'Cantarell', 40 | 'Fira Sans', 41 | 'Droid Sans', 42 | 'Helvetica Neue', 43 | 'sans-serif' 44 | ], 45 | variable: '--font-secondary', 46 | subsets: ['latin'] 47 | }); 48 | 49 | export const sourceCodeProFont = Source_Code_Pro({ 50 | preload: false, 51 | weight: ['400', '500', '600', '700'], 52 | fallback: ['monospace'], 53 | variable: '--font-code', 54 | subsets: ['latin'] 55 | }); 56 | -------------------------------------------------------------------------------- /components/elements/section-wrapper/section-wrapper.module.scss: -------------------------------------------------------------------------------- 1 | @import '@/styles/variables.scss'; 2 | @import '@/styles/media.scss'; 3 | 4 | .sectionWrapper { 5 | width: 100%; 6 | display: grid; 7 | place-items: center; 8 | position: relative; 9 | overflow-x: clip; 10 | background-color: #f9fafb; 11 | padding: 0; 12 | background-image: radial-gradient(circle, rgba(207, 221, 248, .302) 2px, transparent 0); 13 | background-size: 30px 30px; 14 | background-repeat: repeat; 15 | background-position: 0 0; 16 | min-height: 400px; 17 | display: flex; 18 | align-items: center; 19 | 20 | .section { 21 | position: relative; 22 | width: 100%; 23 | max-width: 1200px; 24 | margin: 0 auto; 25 | padding: 2rem 1rem; 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | 30 | &.dark { 31 | color: #fff; 32 | background-color: #111827; 33 | } 34 | 35 | &.light { 36 | color: #111827; 37 | background-color: transparent; 38 | } 39 | } 40 | 41 | &.pushUp { 42 | margin-top: -144px; 43 | 44 | @media (max-width: 640px) { 45 | margin-top: -128px; 46 | } 47 | } 48 | } 49 | 50 | @media (max-width: 767px) { 51 | .section { 52 | padding: 2rem 1rem; 53 | } 54 | } 55 | 56 | @media (min-width: 768px) { 57 | .section { 58 | padding: 2rem 2rem; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![google-one-tap](https://github.com/user-attachments/assets/9c5992cd-dd4f-4abd-b89a-9f8721030f1a) 2 | 3 | # Descope's Google One Tap Sample App 4 | 5 | Welcome to Descope's Google One Tap Sample App, built with [NextJS](https://nextjs.org/) and Descope-powered [Google One Tap](https://developers.google.com/identity/gsi/web/guides/display-google-one-tap). 6 | 7 | 8 | # Installation 9 | 10 | 1. Clone the repository: 11 | 12 | ``` 13 | git clone https://github.com/descope-sample-apps/onetap.git 14 | ``` 15 | 16 | 2. Install dependencies: 17 | 18 | ``` 19 | npm install 20 | ``` 21 | 22 | 3. Setup environment variables: 23 | 24 | ``` 25 | NEXT_PUBLIC_DESCOPE_PROJECT_ID="YOUR_DESCOPE_PROJECT_ID" // Required 26 | ``` 27 | 28 | Use `.env.example` and rename to `.env.local` after adding the environment variables. 29 | 30 | > **_NOTE:_** NEXT_PUBLIC_DESCOPE_PROJECT_ID is your Descope Project ID which you can find under [Project Settings](https://app.descope.com/settings/project), in the console. 31 | 32 | 4. Set up a Google One Tap specific Custom Provider in the Descope console (Dashboard -> Authentication Methods -> Social Login -> + Custom Provider) and name it `google-implicit`. 33 | 34 | ## Running the Application 35 | 36 | To start the application, run: 37 | 38 | ``` 39 | npm run dev 40 | ``` 41 | 42 | Navigate to `http://localhost:3000/` in your browser. 43 | 44 | ## 📜 License 45 | 46 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 47 | -------------------------------------------------------------------------------- /components/elements/section-wrapper/section-wrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import clsx from 'clsx'; 4 | import { usePathname } from 'next/navigation'; 5 | import { ElementType } from 'react'; 6 | import GradientOverlay from '../gradient-overlay'; 7 | 8 | import styles from './section-wrapper.module.scss'; 9 | import type { SectionWrapperProps } from './section-wrapper.types'; 10 | 11 | const SectionWrapper = ({ 12 | as, 13 | className, 14 | classNameWrapper, 15 | order, 16 | children, 17 | gradient, 18 | gradientMobile, 19 | gradientClassname, 20 | ref, 21 | ...restProps 22 | }: SectionWrapperProps) => { 23 | const pathname = usePathname(); 24 | const Component: ElementType = as || 'section'; 25 | const isHome = pathname === '/'; 26 | const finalGradient = gradient; 27 | 28 | return ( 29 |
36 | 44 | {children} 45 | 46 | {finalGradient ? ( 47 | 52 | ) : ( 53 | // add noise to match section with gradients when is dark 54 | 58 | )} 59 |
60 | ); 61 | }; 62 | 63 | export default SectionWrapper; 64 | -------------------------------------------------------------------------------- /components/elements/section-divider/section-divider.module.scss: -------------------------------------------------------------------------------- 1 | @import '@/styles/variables.scss'; 2 | @import '@/styles/media.scss'; 3 | 4 | .wrapper { 5 | height: 7vw; 6 | max-height: 58px; 7 | 8 | @include media(sm) { 9 | height: 5vw; 10 | } 11 | 12 | @include media(lg) { 13 | height: 4vw; 14 | } 15 | } 16 | 17 | .wrapper[data-rotated='true'] { 18 | box-shadow: 0 1px 0 $grey-950; 19 | } 20 | 21 | .divider { 22 | height: 100%; 23 | background-color: $grey-25; 24 | position: relative; 25 | z-index: 0; 26 | transform: translateY(1px); 27 | clip-path: polygon( 28 | -0.008% 78.072%, 29 | 9.847% 78.072%, 30 | 9.847% 78.072%, 31 | 10.527% 77.753%, 32 | 11.192% 76.812%, 33 | 11.837% 75.272%, 34 | 12.458% 73.158%, 35 | 13.05% 70.493%, 36 | 13.608% 67.301%, 37 | 14.127% 63.607%, 38 | 14.604% 59.433%, 39 | 15.033% 54.804%, 40 | 15.409% 49.745%, 41 | 16.827% 28.328%, 42 | 16.827% 28.328%, 43 | 17.204% 23.268%, 44 | 17.633% 18.639%, 45 | 18.109% 14.465%, 46 | 18.629% 10.771%, 47 | 19.187% 7.579%, 48 | 19.779% 4.914%, 49 | 20.4% 2.8%, 50 | 21.045% 1.26%, 51 | 21.71% 0.319%, 52 | 22.389% 0%, 53 | 77.603% 0%, 54 | 77.603% 0%, 55 | 78.282% 0.319%, 56 | 78.947% 1.26%, 57 | 79.592% 2.8%, 58 | 80.213% 4.914%, 59 | 80.805% 7.579%, 60 | 81.363% 10.771%, 61 | 81.883% 14.465%, 62 | 82.359% 18.639%, 63 | 82.788% 23.268%, 64 | 83.165% 28.328%, 65 | 84.583% 49.745%, 66 | 84.583% 49.745%, 67 | 84.959% 54.804%, 68 | 85.388% 59.433%, 69 | 85.865% 63.607%, 70 | 86.384% 67.301%, 71 | 86.942% 70.493%, 72 | 87.534% 73.158%, 73 | 88.155% 75.272%, 74 | 88.8% 76.812%, 75 | 89.465% 77.753%, 76 | 90.144% 78.072%, 77 | 100% 78.072%, 78 | 100% 100%, 79 | -0.008% 100%, 80 | -0.008% 78.072% 81 | ); 82 | } 83 | 84 | .wrapper[data-rotated='true'] .divider { 85 | transform: rotate(180deg) translateY(1px); 86 | } 87 | -------------------------------------------------------------------------------- /components/features/message.tsx: -------------------------------------------------------------------------------- 1 | import LogoutButton from "./logout-button"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import { CardContainer } from "../ui/3d-card"; 5 | import { AuthenticationInfo } from "./descope-types"; 6 | 7 | 8 | export default function Message({ session }: { session: AuthenticationInfo | undefined }) { 9 | return 10 | {session ?
11 |

12 | {`Welcome${session.token.givenName ? `, ${session.token.givenName}` : ""}!`} 13 |

14 |

You've just experienced Descope's "One Tap" Login. Feel free to log out and try it again.

15 | 16 |
:
17 |

Experience Google "One Tap" Login

18 |
19 |

Log in with a single click! Look at the top-right corner of your screen.

20 |

Not seeing the "One Tap" Login popup?

21 |

• Make sure you have an active Chrome / Gmail session.

22 |

• You might be on an unsupported browser. Check out the list here.

23 |
24 |
} 25 |
26 | } 27 | 28 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 222.2 47.4% 11.2%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 222.2 84% 4.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 222.2 84% 4.9%; 36 | --foreground: 210 40% 98%; 37 | --card: 222.2 84% 4.9%; 38 | --card-foreground: 210 40% 98%; 39 | --popover: 222.2 84% 4.9%; 40 | --popover-foreground: 210 40% 98%; 41 | --primary: 210 40% 98%; 42 | --primary-foreground: 222.2 47.4% 11.2%; 43 | --secondary: 217.2 32.6% 17.5%; 44 | --secondary-foreground: 210 40% 98%; 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | --accent: 217.2 32.6% 17.5%; 48 | --accent-foreground: 210 40% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 217.2 32.6% 17.5%; 52 | --input: 217.2 32.6% 17.5%; 53 | --ring: 212.7 26.8% 83.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /components/features/one-tap-component.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useDescope, useSession } from "@descope/nextjs-sdk/client"; 4 | import { useRouter } from "next/navigation"; 5 | import { useEffect } from "react"; 6 | 7 | let oneTapInitialized = false; 8 | 9 | const OneTapComp = () => { 10 | const oneTap = true; 11 | const sdk = useDescope(); 12 | const router = useRouter(); 13 | const { isAuthenticated, isSessionLoading } = useSession(); 14 | 15 | const OneTapConfig: { 16 | //auto_select: boolean; 17 | //prompt_parent_id: string; 18 | //cancel_on_tap_outside: boolean; 19 | //context: "signup" | "signin" | "use" | undefined; 20 | //intermediate_iframe_close_callback: any; 21 | //itp_support: boolean; 22 | //login_hint: string; 23 | //hd: string; 24 | use_fedcm_for_prompt?: boolean; 25 | } = { 26 | //auto_select: true, 27 | //prompt_parent_id: "example-id", 28 | //cancel_on_tap_outside: false, 29 | //context: "signin", 30 | //intermediate_iframe_close_callback: logBeforeClose, 31 | //itp_support: true, 32 | //login_hint: "user@example.com", 33 | //hd: "example.com", 34 | use_fedcm_for_prompt: false, 35 | }; 36 | 37 | const startOneTap = async () => { 38 | // eslint-disable-next-line 39 | if (oneTapInitialized) return; 40 | 41 | const isLoggedIn = await sdk.fedcm.isLoggedIn(); 42 | const isSupported = await sdk.fedcm.isSupported(); 43 | 44 | const res: any = await sdk.fedcm.oneTap( 45 | "google", 46 | {use_fedcm_for_prompt: true}, 47 | {}, 48 | () => { 49 | console.log("One-Tap sign-in was skipped"); 50 | }, 51 | () => { 52 | console.log("One-Tap sign-in was dismissed."); 53 | } 54 | ); 55 | if (isLoggedIn && isSupported) { 56 | console.log("One-Tap sign-in was actually shown."); 57 | } 58 | 59 | router.refresh(); 60 | oneTapInitialized = true; 61 | }; 62 | 63 | useEffect(() => { 64 | if (oneTap && !isAuthenticated && !isSessionLoading) { 65 | startOneTap(); 66 | } 67 | }, [isAuthenticated, isSessionLoading]); 68 | 69 | // Return some JSX here. For example, return null if there's nothing to render: 70 | return null; 71 | }; 72 | 73 | export default OneTapComp; 74 | -------------------------------------------------------------------------------- /styles/_variables.scss: -------------------------------------------------------------------------------- 1 | // ~~~~~~~~~~~~~ Colors ~~~~~~~~~~~~~ 2 | $white: #ffffff; 3 | $off-white: #ededf5; 4 | $deep-blue: #0082b5; 5 | 6 | $link: $deep-blue; 7 | 8 | $red: #ed404a; 9 | $cyan: #0ed7d1; 10 | $cyan-bright: #0ed8d2; 11 | $dark-green: #65ca29; 12 | $deep-green: #65cc29; 13 | 14 | $text-primary: $white; 15 | $text-secondary: $cyan; 16 | 17 | // ~~~~~~~ REFRESH DESIGN COLORS ~~~~~~~ 18 | $animation-duration: 400ms; 19 | $animation-function: cubic-bezier(0.59, 0, 0.06, 1); 20 | 21 | $grey-0: #ffffff; 22 | $grey-25: #f9fafb; 23 | $grey-50: #f0f0f1; 24 | $grey-100: #dadee5; 25 | $grey-200: #c3c5c9; 26 | $grey-300: #a4a8ae; 27 | $grey-400: #868b93; 28 | $grey-500: #686e78; 29 | $grey-800: #2a2d33; 30 | $grey-950: #0a101a; 31 | $grey-950-opacity: rgba(10, 16, 26, 0.85); 32 | $grey-opacity: rgba(12, 19, 31, 0.5); 33 | 34 | $accents-cyan-300: #9dfafa; 35 | $accents-cyan-500: #7deded; 36 | 37 | $accents-blue-100: #dadee5; 38 | $accents-blue-500: #0085ff; 39 | $accents-blue-600: #0085ff; 40 | $accents-blue-700: #334665; 41 | $accents-blue-800: #253754; 42 | $accents-blue-900: #111e32; 43 | 44 | $orange: #c94900; 45 | $green: #0ad012; 46 | 47 | $button-surface-primary-button-surface-primary-default: $accents-cyan-500; 48 | $button-surface-primary-button-surface-primary-hover: $accents-cyan-300; 49 | $button-border-primary-button-border-primary-default: $accents-cyan-300; 50 | $button-border-primary-button-border-primary-hover: $accents-cyan-300; 51 | $button-border-primary-button-border-primary-focus: $accents-blue-500; 52 | $button-border-secondary-button-border-secondary-focus: $accents-blue-500; 53 | $button-text-primary-button-text-primary-default: $grey-950; 54 | $button-text-primary-button-text-primary-inactive: $grey-400; 55 | $icon-icon-black: $grey-950; 56 | $icon-icon-blue: $accents-blue-500; 57 | $icon-icon-dark-grey: $grey-200; 58 | $icon-icon-white: $grey-0; 59 | $icon-icon-inactive: $grey-400; 60 | $shadow-light-card: 61 | 0px 12px 12px -6px rgba(14, 63, 126, 0.04), 62 | 0px 0px 0px 1px rgba(14, 63, 126, 0.04), 63 | 0px 6px 6px -3px rgba(42, 51, 69, 0.04), 64 | 0px 3px 3px -1.5px rgba(42, 51, 69, 0.04), 65 | 0px 1px 1px -0.5px rgba(42, 51, 69, 0.04); 66 | 67 | $primitive-red: #e34242; 68 | $primitive-orange: #c94900; 69 | // ~~~~~~~ REFRESH DESIGN COLORS END ~~~~~~~ 70 | 71 | $passwordle-green: #70f02e; 72 | $passwordle-yellow: #141412; 73 | 74 | $primary-10: #c8c8cd; 75 | $primary-20: #c6c6cb; 76 | $primary-30: #8b8b98; 77 | $primary-35: #707070; 78 | $primary-40: #646469; 79 | $primary-60: #404051; 80 | $primary-70: #2f2f3c; 81 | $primary-80: #1c1c23; 82 | $primary-100: #121217; 83 | $primary-110: #0d0d11; 84 | $primary-120: #0e0e11; 85 | 86 | $surface-background-light: $grey-25; 87 | $surface-background-dark: $grey-950; 88 | -------------------------------------------------------------------------------- /components/elements/gradient-overlay/gradient-overlay.module.scss: -------------------------------------------------------------------------------- 1 | .gradientWrapper { 2 | position: absolute; 3 | top: 0; 4 | left: 50%; 5 | width: 100vw; 6 | z-index: 2; 7 | transform: translateX(-50%); 8 | pointer-events: none; 9 | 10 | .gradient { 11 | pointer-events: none; 12 | width: 100vw; 13 | height: auto; 14 | z-index: 2; 15 | } 16 | } 17 | 18 | .noise { 19 | position: absolute; 20 | inset: 0; 21 | z-index: 2; 22 | pointer-events: none; 23 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAUVBMVEWFhYWDg4N3d3dtbW17e3t1dXWBgYGHh4d5eXlzc3OLi4ubm5uVlZWPj4+NjY19fX2JiYl/f39ra2uRkZGZmZlpaWmXl5dvb29xcXGTk5NnZ2c8TV1mAAAAG3RSTlNAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAvEOwtAAAFVklEQVR4XpWWB67c2BUFb3g557T/hRo9/WUMZHlgr4Bg8Z4qQgQJlHI4A8SzFVrapvmTF9O7dmYRFZ60YiBhJRCgh1FYhiLAmdvX0CzTOpNE77ME0Zty/nWWzchDtiqrmQDeuv3powQ5ta2eN0FY0InkqDD73lT9c9lEzwUNqgFHs9VQce3TVClFCQrSTfOiYkVJQBmpbq2L6iZavPnAPcoU0dSw0SUTqz/GtrGuXfbyyBniKykOWQWGqwwMA7QiYAxi+IlPdqo+hYHnUt5ZPfnsHJyNiDtnpJyayNBkF6cWoYGAMY92U2hXHF/C1M8uP/ZtYdiuj26UdAdQQSXQErwSOMzt/XWRWAz5GuSBIkwG1H3FabJ2OsUOUhGC6tK4EMtJO0ttC6IBD3kM0ve0tJwMdSfjZo+EEISaeTr9P3wYrGjXqyC1krcKdhMpxEnt5JetoulscpyzhXN5FRpuPHvbeQaKxFAEB6EN+cYN6xD7RYGpXpNndMmZgM5Dcs3YSNFDHUo2LGfZuukSWyUYirJAdYbF3MfqEKmjM+I2EfhA94iG3L7uKrR+GdWD73ydlIB+6hgref1QTlmgmbM3/LeX5GI1Ux1RWpgxpLuZ2+I+IjzZ8wqE4nilvQdkUdfhzI5QDWy+kw5Wgg2pGpeEVeCCA7b85BO3F9DzxB3cdqvBzWcmzbyMiqhzuYqtHRVG2y4x+KOlnyqla8AoWWpuBoYRxzXrfKuILl6SfiWCbjxoZJUaCBj1CjH7GIaDbc9kqBY3W/Rgjda1iqQcOJu2WW+76pZC9QG7M00dffe9hNnseupFL53r8F7YHSwJWUKP2q+k7RdsxyOB11n0xtOvnW4irMMFNV4H0uqwS5ExsmP9AxbDTc9JwgneAT5vTiUSm1E7BSflSt3bfa1tv8Di3R8n3Af7MNWzs49hmauE2wP+ttrq+AsWpFG2awvsuOqbipWHgtuvuaAE+A1Z/7gC9hesnr+7wqCwG8c5yAg3AL1fm8T9AZtp/bbJGwl1pNrE7RuOX7PeMRUERVaPpEs+yqeoSmuOlokqw49pgomjLeh7icHNlG19yjs6XXOMedYm5xH2YxpV2tc0Ro2jJfxC50ApuxGob7lMsxfTbeUv07TyYxpeLucEH1gNd4IKH2LAg5TdVhlCafZvpskfncCfx8pOhJzd76bJWeYFnFciwcYfubRc12Ip/ppIhA1/mSZ/RxjFDrJC5xifFjJpY2Xl5zXdguFqYyTR1zSp1Y9p+tktDYYSNflcxI0iyO4TPBdlRcpeqjK/piF5bklq77VSEaA+z8qmJTFzIWiitbnzR794USKBUaT0NTEsVjZqLaFVqJoPN9ODG70IPbfBHKK+/q/AWR0tJzYHRULOa4MP+W/HfGadZUbfw177G7j/OGbIs8TahLyynl4X4RinF793Oz+BU0saXtUHrVBFT/DnA3ctNPoGbs4hRIjTok8i+algT1lTHi4SxFvONKNrgQFAq2/gFnWMXgwffgYMJpiKYkmW3tTg3ZQ9Jq+f8XN+A5eeUKHWvJWJ2sgJ1Sop+wwhqFVijqWaJhwtD8MNlSBeWNNWTa5Z5kPZw5+LbVT99wqTdx29lMUH4OIG/D86ruKEauBjvH5xy6um/Sfj7ei6UUVk4AIl3MyD4MSSTOFgSwsH/QJWaQ5as7ZcmgBZkzjjU1UrQ74ci1gWBCSGHtuV1H2mhSnO3Wp/3fEV5a+4wz//6qy8JxjZsmxxy5+4w9CDNJY09T072iKG0EnOS0arEYgXqYnXcYHwjTtUNAcMelOd4xpkoqiTYICWFq0JSiPfPDQdnt+4/wuqcXY47QILbgAAAABJRU5ErkJggg==); 24 | mix-blend-mode: hard-light; 25 | background-size: 35px; 26 | opacity: 0.8; 27 | } 28 | 29 | @media (max-width: 767px) { 30 | .desktop { 31 | display: none; 32 | } 33 | } 34 | 35 | @media (min-width: 768px) { 36 | .mobile { 37 | display: none; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | moveHorizontal: { 71 | "0%": { 72 | transform: "translateX(-50%) translateY(-10%)", 73 | }, 74 | "50%": { 75 | transform: "translateX(50%) translateY(10%)", 76 | }, 77 | "100%": { 78 | transform: "translateX(-50%) translateY(-10%)", 79 | }, 80 | }, 81 | moveInCircle: { 82 | "0%": { 83 | transform: "rotate(0deg)", 84 | }, 85 | "50%": { 86 | transform: "rotate(180deg)", 87 | }, 88 | "100%": { 89 | transform: "rotate(360deg)", 90 | }, 91 | }, 92 | moveVertical: { 93 | "0%": { 94 | transform: "translateY(-50%)", 95 | }, 96 | "50%": { 97 | transform: "translateY(50%)", 98 | }, 99 | "100%": { 100 | transform: "translateY(-50%)", 101 | }, 102 | }, 103 | }, 104 | animation: { 105 | "accordion-down": "accordion-down 0.2s ease-out", 106 | "accordion-up": "accordion-up 0.2s ease-out", 107 | first: "moveVertical 30s ease infinite", 108 | second: "moveInCircle 20s reverse infinite", 109 | third: "moveInCircle 40s linear infinite", 110 | fourth: "moveHorizontal 40s ease infinite", 111 | fifth: "moveInCircle 20s ease infinite", 112 | 113 | }, 114 | }, 115 | }, 116 | plugins: [require("tailwindcss-animate")], 117 | } satisfies Config 118 | 119 | export default config -------------------------------------------------------------------------------- /styles/_theme.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $themes: ( 4 | light: ( 5 | surface-surface-background: $grey-25, 6 | text-text-primary: $grey-950, 7 | text-text-muted: $grey-500, 8 | text-text-accent: $accents-blue-600, 9 | card-surface-card-surface-default: $grey-0, 10 | card-border-card-border-default: $grey-100, 11 | card-border-card-border-hover: $accents-blue-600, 12 | button-surface-primary-button-surface-primary-inactive: $grey-100, 13 | button-surface-secondary-button-surface-secondary-default: $grey-0, 14 | button-surface-secondary-button-surface-secondary-hover: $grey-50, 15 | button-surface-secondary-button-surface-secondary-inactive: $grey-50, 16 | button-border-secondary-button-border-secondary-default: $grey-100, 17 | button-border-secondary-button-border-secondary-hover: $grey-200, 18 | button-border-secondary-button-border-secondary-inactive: $grey-100, 19 | button-text-secondary-button-text-secondary-default: $grey-950, 20 | button-text-secondary-button-text-secondary-inactive: $grey-300, 21 | icon-icon-light-grey: $grey-500, 22 | button-text-tertiary-tertiary-default: $grey-950, 23 | button-text-tertiary-tertiary-hover: $accents-blue-600, 24 | button-text-tertiary-tertiary-inactive: $grey-300, 25 | icon-icon-blue: $accents-blue-600, 26 | icon-primary: $grey-950, 27 | surface-surface-dark-blue: $grey-50, 28 | input-surface-input-surface-default: $grey-25, 29 | input-border-input-border-default: $grey-50, 30 | input-border-input-border-hover: $accents-blue-600, 31 | input-border-input-border-focus: #dfe7fb, 32 | input-surface-input-surface-hover: $grey-0, 33 | input-surface-input-surface-inactive: $grey-0, 34 | text-text-inactive: $grey-300, 35 | icon-icon-cyan: $accents-cyan-500, 36 | text-highlight-color: $accents-blue-600, 37 | red: #b50808, 38 | red-light: #fff0f0 39 | ), 40 | dark: ( 41 | surface-surface-background: $grey-950, 42 | text-text-primary: $grey-0, 43 | text-text-muted: $grey-200, 44 | text-text-accent: $accents-cyan-500, 45 | card-surface-card-surface-default: $grey-opacity, 46 | card-border-card-border-default: $grey-800, 47 | card-border-card-border-hover: $accents-cyan-500, 48 | button-surface-primary-button-surface-primary-inactive: $grey-100, 49 | button-surface-secondary-button-surface-secondary-default: $accents-blue-800, 50 | button-surface-secondary-button-surface-secondary-hover: $accents-blue-700, 51 | button-surface-secondary-button-surface-secondary-inactive: 52 | $accents-blue-900, 53 | button-border-secondary-button-border-secondary-default: $accents-blue-700, 54 | button-border-secondary-button-border-secondary-hover: $accents-blue-800, 55 | button-border-secondary-button-border-secondary-inactive: $accents-blue-800, 56 | button-text-secondary-button-text-secondary-default: $grey-0, 57 | button-text-secondary-button-text-secondary-inactive: $grey-300, 58 | icon-icon-light-grey: $grey-50, 59 | button-text-tertiary-tertiary-default: $grey-0, 60 | button-text-tertiary-tertiary-hover: $accents-blue-500, 61 | button-text-tertiary-tertiary-inactive: $grey-400, 62 | icon-icon-blue: $accents-blue-500, 63 | icon-primary: $grey-0, 64 | surface-surface-dark-blue: $accents-blue-900, 65 | input-surface-input-surface-default: $accents-blue-900, 66 | input-border-input-border-default: $grey-800, 67 | input-border-input-border-hover: $accents-blue-500, 68 | input-border-input-border-focus: #0c1935, 69 | input-surface-input-surface-hover: $accents-blue-900, 70 | input-surface-input-surface-inactive: $grey-950, 71 | text-text-inactive: $grey-500, 72 | icon-icon-cyan: $accents-cyan-300, 73 | text-highlight-color: $grey-0, 74 | red: #e34242, 75 | red-light: #2c0f0f 76 | ) 77 | ); 78 | 79 | $theme-map: null; 80 | 81 | // to be used in module files like: component/component.module.scss 82 | @mixin theme() { 83 | @each $theme, $map in $themes { 84 | $theme-map: $map !global; 85 | 86 | @if $theme == 'light' { 87 | @content; 88 | } 89 | 90 | :global(.#{$theme}) & { 91 | @content; 92 | } 93 | } 94 | $theme-map: null !global; 95 | } 96 | 97 | // to be used in global files like: styles/_typography.scss 98 | @mixin g_theme() { 99 | @each $theme, $map in $themes { 100 | $theme-map: $map !global; 101 | 102 | @if $theme == 'light' { 103 | @content; 104 | } 105 | 106 | .#{$theme} & { 107 | @content; 108 | } 109 | } 110 | $theme-map: null !global; 111 | } 112 | 113 | @function theme-get($key) { 114 | @return map.get($theme-map, $key); 115 | } 116 | -------------------------------------------------------------------------------- /components/ui/3d-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import React, { 5 | createContext, 6 | useState, 7 | useContext, 8 | useRef, 9 | useEffect, 10 | } from "react"; 11 | 12 | const MouseEnterContext = createContext< 13 | [boolean, React.Dispatch>] | undefined 14 | >(undefined); 15 | 16 | export const CardContainer = ({ 17 | children, 18 | className, 19 | containerClassName, 20 | }: { 21 | children?: React.ReactNode; 22 | className?: string; 23 | containerClassName?: string; 24 | }) => { 25 | const containerRef = useRef(null); 26 | const [isMouseEntered, setIsMouseEntered] = useState(false); 27 | 28 | const handleMouseMove = (e: React.MouseEvent) => { 29 | if (!containerRef.current) return; 30 | const { left, top, width, height } = 31 | containerRef.current.getBoundingClientRect(); 32 | const x = (e.clientX - left - width / 2) / 25; 33 | const y = (e.clientY - top - height / 2) / 25; 34 | containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`; 35 | }; 36 | 37 | const handleMouseEnter = (e: React.MouseEvent) => { 38 | setIsMouseEntered(true); 39 | if (!containerRef.current) return; 40 | }; 41 | 42 | const handleMouseLeave = (e: React.MouseEvent) => { 43 | if (!containerRef.current) return; 44 | setIsMouseEntered(false); 45 | containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`; 46 | }; 47 | return ( 48 | 49 |
58 |
71 | {children} 72 |
73 |
74 |
75 | ); 76 | }; 77 | 78 | export const CardBody = ({ 79 | children, 80 | className, 81 | }: { 82 | children: React.ReactNode; 83 | className?: string; 84 | }) => { 85 | return ( 86 |
*]:[transform-style:preserve-3d]", 89 | className 90 | )} 91 | > 92 | {children} 93 |
94 | ); 95 | }; 96 | 97 | export const CardItem = ({ 98 | as: Tag = "div", 99 | children, 100 | className, 101 | translateX = 0, 102 | translateY = 0, 103 | translateZ = 0, 104 | rotateX = 0, 105 | rotateY = 0, 106 | rotateZ = 0, 107 | ...rest 108 | }: { 109 | as?: React.ElementType; 110 | children: React.ReactNode; 111 | className?: string; 112 | translateX?: number | string; 113 | translateY?: number | string; 114 | translateZ?: number | string; 115 | rotateX?: number | string; 116 | rotateY?: number | string; 117 | rotateZ?: number | string; 118 | [key: string]: any; 119 | }) => { 120 | const ref = useRef(null); 121 | const [isMouseEntered] = useMouseEnter(); 122 | 123 | useEffect(() => { 124 | handleAnimations(); 125 | }, [isMouseEntered]); 126 | 127 | const handleAnimations = () => { 128 | if (!ref.current) return; 129 | if (isMouseEntered) { 130 | ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`; 131 | } else { 132 | ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`; 133 | } 134 | }; 135 | 136 | return ( 137 | 142 | {children} 143 | 144 | ); 145 | }; 146 | 147 | // Create a hook to use the context 148 | export const useMouseEnter = () => { 149 | const context = useContext(MouseEnterContext); 150 | if (context === undefined) { 151 | throw new Error("useMouseEnter must be used within a MouseEnterProvider"); 152 | } 153 | return context; 154 | }; 155 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Message from "@/components/features/message"; 2 | import OneTapComp from "@/components/features/one-tap-component"; 3 | import { BackgroundGradientAnimation } from "@/components/ui/background-gradient-animation"; 4 | import { session } from "@descope/nextjs-sdk/server"; 5 | import Image from "next/image"; 6 | import SectionDivider from '@/components/elements/section-divider'; 7 | import SectionWrapper from '@/components/elements/section-wrapper'; 8 | import { CardContainer } from "@/components/ui/3d-card"; 9 | 10 | export default function Home() { 11 | const currSession = session(); 12 | 13 | return ( 14 |
15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 | Descope logo 24 | Descope logo 25 | 26 | 27 |
28 | 29 | 30 |
31 |
32 |

33 | What Is
One Tap
34 |

35 |
36 |

Google One Tap is a sign-in feature that allows users to log into websites or apps with just one click without remembering or entering passwords. 37 | This feature can be embedded into websites and apps, offering a seamless, fast login experience that reduces user friction. It helps businesses increase user sign-ups and engagement by minimizing barriers to entry. 38 |

39 |
40 |
41 | 42 |

Frictionless

43 |

44 | Allow users to log in with one tap without needing to create new credentials. Let users choose between all their Google accounts. 45 |

46 |
47 | 48 |

Secure

49 |

50 | Hand off identity management security to trusted IdPs (Google and Descope). De-risk your org from credential management and storage. 51 |

52 |
53 | 54 |

Compatible

55 |

56 | Reach a wide range of users with an auth method that works seamlessly on web and mobile, and is compatible with all major browsers. 57 |

58 |
59 |
60 | 70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 |
78 |

79 | Want To Add One Tap To Your App? 80 |

81 |
82 |

83 | Descope helps developers easily add any passwordless authentication method (like One Tap) to their apps with a few lines of code. Choose from our drag-and-drop workflows, SDKs, or APIs. 84 |

85 |
86 | 96 |
97 |
98 |
99 |
100 |
101 |
102 | ); 103 | } 104 | 105 | -------------------------------------------------------------------------------- /components/ui/background-gradient-animation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { useEffect, useRef, useState } from "react"; 4 | 5 | export const BackgroundGradientAnimation = ({ 6 | gradientBackgroundStart = "#0f1a2e", 7 | gradientBackgroundEnd = "#1b2c45", 8 | firstColor = "33, 255, 181", 9 | secondColor = "30, 100, 110", 10 | thirdColor = "25, 80, 100", 11 | fourthColor = "20, 40, 80", 12 | fifthColor = "15, 30, 50", 13 | pointerColor = "33, 255, 181", 14 | size = "50%", 15 | blendingValue = "hard-light", 16 | children, 17 | className, 18 | interactive = true, 19 | containerClassName, 20 | }: { 21 | gradientBackgroundStart?: string; 22 | gradientBackgroundEnd?: string; 23 | firstColor?: string; 24 | secondColor?: string; 25 | thirdColor?: string; 26 | fourthColor?: string; 27 | fifthColor?: string; 28 | pointerColor?: string; 29 | size?: string; 30 | blendingValue?: string; 31 | children?: React.ReactNode; 32 | className?: string; 33 | interactive?: boolean; 34 | containerClassName?: string; 35 | }) => { 36 | const interactiveRef = useRef(null); 37 | 38 | const [curX, setCurX] = useState(0); 39 | const [curY, setCurY] = useState(0); 40 | const [tgX, setTgX] = useState(0); 41 | const [tgY, setTgY] = useState(0); 42 | useEffect(() => { 43 | document.body.style.setProperty( 44 | "--gradient-background-start", 45 | gradientBackgroundStart 46 | ); 47 | document.body.style.setProperty( 48 | "--gradient-background-end", 49 | gradientBackgroundEnd 50 | ); 51 | document.body.style.setProperty("--first-color", firstColor); 52 | document.body.style.setProperty("--second-color", secondColor); 53 | document.body.style.setProperty("--third-color", thirdColor); 54 | document.body.style.setProperty("--fourth-color", fourthColor); 55 | document.body.style.setProperty("--fifth-color", fifthColor); 56 | document.body.style.setProperty("--pointer-color", pointerColor); 57 | document.body.style.setProperty("--size", size); 58 | document.body.style.setProperty("--blending-value", blendingValue); 59 | }, []); 60 | 61 | useEffect(() => { 62 | function move() { 63 | if (!interactiveRef.current) { 64 | return; 65 | } 66 | setCurX(curX + (tgX - curX) / 20); 67 | setCurY(curY + (tgY - curY) / 20); 68 | interactiveRef.current.style.transform = `translate(${Math.round( 69 | curX 70 | )}px, ${Math.round(curY)}px)`; 71 | } 72 | 73 | move(); 74 | }, [tgX, tgY]); 75 | 76 | const handleMouseMove = (event: React.MouseEvent) => { 77 | if (interactiveRef.current) { 78 | const rect = interactiveRef.current.getBoundingClientRect(); 79 | setTgX(event.clientX - rect.left); 80 | setTgY(event.clientY - rect.top); 81 | } 82 | }; 83 | 84 | const [isSafari, setIsSafari] = useState(false); 85 | useEffect(() => { 86 | setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent)); 87 | }, []); 88 | 89 | return ( 90 |
97 | 98 | 99 | 100 | 105 | 111 | 112 | 113 | 114 | 115 |
{children}
116 |
122 |
131 |
140 |
149 |
158 |
167 | 168 | {interactive && ( 169 |
178 | )} 179 |
180 |
181 | ); 182 | }; -------------------------------------------------------------------------------- /public/one_tap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/descope-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/onetap_guru.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | width: 100%; 6 | font-size: $html-font-size; 7 | font-family: var(--font-primary); 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | scroll-padding-top: rem-convert(calc(56px + 14px)); 11 | 12 | @include g_theme { 13 | color: theme-get(text-text-primary); 14 | background-color: theme-get(surface-surface-background); 15 | } 16 | 17 | @include media(sm) { 18 | scroll-padding-top: rem-convert(calc(83px + 17px)); 19 | } 20 | } 21 | 22 | * { 23 | box-sizing: border-box; 24 | color: inherit; 25 | margin: 0; 26 | scroll-behavior: smooth; 27 | } 28 | 29 | table { 30 | border-collapse: collapse; 31 | } 32 | 33 | main { 34 | background-image: radial-gradient(circle, #cfddf84d 2px, transparent 2px); 35 | background-size: 30px 30px; 36 | background-repeat: repeat; 37 | } 38 | 39 | /** 40 | * Correct `block` display not defined in IE 8/9. 41 | */ 42 | article, 43 | aside, 44 | details, 45 | figcaption, 46 | figure, 47 | footer, 48 | header, 49 | hgroup, 50 | main, 51 | nav, 52 | section, 53 | summary { 54 | display: block; 55 | } 56 | 57 | /** 58 | * Correct `inline-block` display not defined in IE 8/9. 59 | */ 60 | audio, 61 | canvas, 62 | video { 63 | display: inline-block; 64 | } 65 | 66 | /** 67 | * Prevent modern browsers from displaying `audio` without controls. 68 | * Remove excess height in iOS 5 devices. 69 | */ 70 | audio:not([controls]) { 71 | display: none; 72 | height: 0; 73 | } 74 | 75 | /** 76 | * Address `[hidden]` styling not present in IE 8/9. 77 | * Hide the `template` element in IE, Safari, and Firefox < 22. 78 | */ 79 | [hidden], 80 | template { 81 | display: none; 82 | } 83 | 84 | /* ========================================================================== 85 | Links 86 | ========================================================================== */ 87 | 88 | /** 89 | * Remove the gray background color from active links in IE 10. 90 | */ 91 | a { 92 | background: transparent; 93 | text-decoration: none; 94 | } 95 | 96 | /** 97 | * Address `outline` inconsistency between Chrome and other browsers. 98 | */ 99 | 100 | button { 101 | background: none; 102 | border: none; 103 | cursor: pointer; 104 | padding: 0; 105 | outline: none; 106 | } 107 | 108 | button, 109 | a { 110 | transition: outline $animation-duration $animation-function; 111 | outline-offset: rem-convert(2px); 112 | 113 | &:focus-visible { 114 | outline-style: solid; 115 | outline-color: $button-border-primary-button-border-primary-focus; 116 | outline-width: rem-convert(2px); 117 | } 118 | } 119 | 120 | /* ========================================================================== 121 | Typography 122 | ========================================================================== */ 123 | 124 | /** 125 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 126 | */ 127 | abbr[title] { 128 | border-bottom: 1px dotted; 129 | } 130 | 131 | /** 132 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 133 | */ 134 | b, 135 | strong { 136 | font-weight: bold; 137 | } 138 | 139 | /** 140 | * Address styling not present in Safari 5 and Chrome. 141 | */ 142 | dfn { 143 | font-style: italic; 144 | } 145 | 146 | /** 147 | * Address differences between Firefox and other browsers. 148 | */ 149 | hr { 150 | box-sizing: content-box; 151 | height: 0; 152 | } 153 | 154 | /** 155 | * Address styling not present in IE 8/9. 156 | */ 157 | mark { 158 | background: #ff0; 159 | color: #000; 160 | } 161 | 162 | /** 163 | * Correct font family set oddly in Safari 5 and Chrome. 164 | */ 165 | code, 166 | kbd, 167 | pre, 168 | samp { 169 | font-family: monospace, serif; 170 | font-size: 1em; 171 | } 172 | 173 | /** 174 | * Improve readability of pre-formatted text in all browsers. 175 | */ 176 | pre { 177 | white-space: pre-wrap; 178 | } 179 | 180 | /** 181 | * Address inconsistent and variable font size in all browsers. 182 | */ 183 | small { 184 | font-size: 80%; 185 | } 186 | 187 | /** 188 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 189 | */ 190 | sub, 191 | sup { 192 | font-size: 75%; 193 | line-height: 0; 194 | position: relative; 195 | vertical-align: baseline; 196 | } 197 | 198 | sup { 199 | top: -0.5em; 200 | } 201 | 202 | sub { 203 | bottom: -0.25em; 204 | } 205 | 206 | /* ========================================================================== 207 | Embedded content 208 | ========================================================================== */ 209 | 210 | /** 211 | * Remove border when inside `a` element in IE 8/9. 212 | */ 213 | img { 214 | border: 0; 215 | } 216 | 217 | /** 218 | * Correct overflow displayed oddly in IE 9. 219 | */ 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /** 229 | * Address margin not present in IE 8/9 and Safari 5. 230 | */ 231 | figure { 232 | margin: 0; 233 | } 234 | 235 | /* ========================================================================== 236 | Forms 237 | ========================================================================== */ 238 | 239 | /** 240 | * Define consistent border, margin, and padding. 241 | */ 242 | fieldset { 243 | border: 1px solid #c0c0c0; 244 | margin: 0 2px; 245 | padding: 0.35em 0.63em 0.75em; 246 | } 247 | 248 | /** 249 | * 1. Correct `color` not being inherited in IE 8/9. 250 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 251 | */ 252 | legend { 253 | border: 0; 254 | padding: 0; 255 | } 256 | 257 | /** 258 | * 1. Correct font family not being inherited in all browsers. 259 | * 2. Correct font size not being inherited in all browsers. 260 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 261 | */ 262 | button, 263 | input, 264 | select, 265 | textarea { 266 | font-family: inherit; 267 | font-size: 100%; 268 | margin: 0; 269 | } 270 | 271 | /** 272 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 273 | * the UA stylesheet. 274 | */ 275 | button, 276 | input { 277 | line-height: normal; 278 | } 279 | 280 | /** 281 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 282 | * All other form control elements do not inherit `text-transform` values. 283 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 284 | * Correct `select` style inheritance in Firefox 4+ and Opera. 285 | */ 286 | button, 287 | select { 288 | text-transform: none; 289 | } 290 | 291 | /** 292 | * Re-set default cursor for disabled elements. 293 | */ 294 | button[disabled], 295 | html input[disabled] { 296 | cursor: default; 297 | } 298 | 299 | /** 300 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 301 | * 2. Remove excess padding in IE 8/9/10. 302 | */ 303 | input[type='checkbox'], 304 | input[type='radio'] { 305 | box-sizing: border-box; 306 | padding: 0; 307 | } 308 | 309 | /** 310 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 311 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 312 | * (include `-moz` to future-proof). 313 | */ 314 | input[type='search'] { 315 | appearance: textfield; 316 | box-sizing: content-box; 317 | } 318 | 319 | /** 320 | * Remove inner padding and search cancel button in Safari 5 and Chrome 321 | * on OS X. 322 | */ 323 | input[type='search']::-webkit-search-cancel-button, 324 | input[type='search']::-webkit-search-decoration { 325 | appearance: none; 326 | } 327 | 328 | /** 329 | * Remove inner padding and border in Firefox 4+. 330 | */ 331 | button::-moz-focus-inner, 332 | input::-moz-focus-inner { 333 | border: 0; 334 | padding: 0; 335 | } 336 | 337 | /** 338 | * 1. Remove default vertical scrollbar in IE 8/9. 339 | * 2. Improve readability and alignment in all browsers. 340 | */ 341 | textarea { 342 | overflow: auto; 343 | 344 | /* 1 */ 345 | vertical-align: top; 346 | 347 | /* 2 */ 348 | } 349 | 350 | code { 351 | font-family: var(--font-code); 352 | } 353 | 354 | .visually-hidden { 355 | border: 0; 356 | clip: rect(0 0 0 0); 357 | height: 1px; 358 | overflow: hidden; 359 | padding: 0; 360 | position: absolute; 361 | white-space: nowrap; 362 | width: 1px; 363 | } 364 | 365 | .osano-cm-window__widget.osano-cm-widget { 366 | display: none !important; 367 | min-height: 0 !important; 368 | } 369 | 370 | .osano-cm-info-dialog__info:not(.osano-cm-info--open) { 371 | visibility: hidden; 372 | transform: translate(-100%); 373 | } 374 | 375 | .osano-cm-dialog.osano-cm-dialog--hidden { 376 | visibility: hidden; 377 | transition-delay: 0ms; 378 | transition-duration: 0ms; 379 | } 380 | 381 | .yt-lite { 382 | background: #121217; 383 | max-width: 100%; 384 | position: relative; 385 | display: block; 386 | contain: content; 387 | background-position: center center; 388 | background-size: cover; 389 | cursor: pointer; 390 | border-radius: rem-convert(5px); 391 | margin-top: rem-convert(31px); 392 | outline-width: rem-convert(7px); 393 | outline-color: rgba(255, 255, 255, 0.15); 394 | outline-style: solid; 395 | 396 | @include media(sm) { 397 | outline-width: rem-convert(12px); 398 | border-radius: rem-convert(8px); 399 | width: rem-convert(560px); 400 | height: rem-convert(315px); 401 | margin-top: rem-convert(55px); 402 | } 403 | 404 | @include mediaMax(sm) { 405 | width: 100%; 406 | aspect-ratio: 1.87/1; 407 | } 408 | } 409 | 410 | /* gradient */ 411 | .yt-lite::before { 412 | content: ''; 413 | display: block; 414 | position: absolute; 415 | top: 0; 416 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==); 417 | background-position: top; 418 | background-repeat: repeat-x; 419 | height: 60px; 420 | padding-bottom: 50px; 421 | width: 100%; 422 | transition: all $animation-duration $animation-function; 423 | } 424 | 425 | /* responsive iframe with a 16:9 aspect ratio 426 | thanks https://css-tricks.com/responsive-iframes/ 427 | */ 428 | .yt-lite::after { 429 | content: ''; 430 | display: block; 431 | padding-bottom: calc(100% / (16 / 9)); 432 | } 433 | .yt-lite > iframe { 434 | width: 100%; 435 | height: 100%; 436 | position: absolute; 437 | top: 0; 438 | left: 0; 439 | } 440 | 441 | /* play button */ 442 | .yt-lite > .lty-playbtn { 443 | position: absolute; 444 | inset: 0; 445 | transition: border-color $animation-duration $animation-function; 446 | border: 4px solid transparent; 447 | 448 | &:before { 449 | content: ''; 450 | position: absolute; 451 | top: 50%; 452 | left: 50%; 453 | transform: translate3d(-50%, -50%, 0); 454 | background-image: url('../public/icons/play-button.svg'); 455 | background-size: 64px 64px; 456 | width: 64px; 457 | height: 64px; 458 | } 459 | 460 | &:focus { 461 | border-color: $button-border-primary-button-border-primary-focus; 462 | } 463 | } 464 | 465 | /* Post-click styles */ 466 | .yt-lite.lyt-activated { 467 | cursor: unset; 468 | } 469 | .yt-lite.lyt-activated::before, 470 | .yt-lite.lyt-activated > .lty-playbtn { 471 | opacity: 0; 472 | pointer-events: none; 473 | } 474 | 475 | .sr-only { 476 | position: absolute; 477 | width: 1px; 478 | height: 1px; 479 | padding: 0; 480 | margin: -1px; 481 | overflow: hidden; 482 | clip: rect(0, 0, 0, 0); 483 | white-space: nowrap; 484 | border-width: 0; 485 | } 486 | 487 | .edges-mask { 488 | mask: linear-gradient( 489 | to right, 490 | transparent 0%, 491 | black 10%, 492 | black 90%, 493 | transparent 100% 494 | ); 495 | } 496 | 497 | .pointer-events-none { 498 | pointer-events: none; 499 | } 500 | 501 | .main-container-blur { 502 | backdrop-filter: blur(16px) saturate(180%); 503 | position: fixed; 504 | left: 0; 505 | top: rem-convert(118px); 506 | width: 100%; 507 | height: 100%; 508 | pointer-events: none; 509 | z-index: 9; 510 | opacity: 0; 511 | background-color: rgba(10, 11, 12, 0.6); 512 | transition-property: opacity; 513 | transition-duration: $animation-duration; 514 | transition-timing-function: $animation-function; 515 | transform: translateZ(0); 516 | 517 | &.visible { 518 | opacity: 1; 519 | } 520 | 521 | &.isScrolled { 522 | top: rem-convert(70px); 523 | } 524 | } 525 | 526 | a.skip-button { 527 | position: absolute; 528 | left: 50%; 529 | top: -16rem; 530 | transform: translateX(-50%); 531 | transition: top 400ms ease-in; 532 | z-index: 99; 533 | } 534 | 535 | a.skip-button:focus-within, 536 | a.skip-button:focus { 537 | left: 50%; 538 | top: 1rem; 539 | transform: translateX(-50%); 540 | } 541 | --------------------------------------------------------------------------------