diff --git a/apps/epic-react/public/assets/flying-rocket-light-sm@2x.webp b/apps/epic-react/public/assets/flying-rocket-light-sm@2x.webp new file mode 100644 index 000000000..6cec2d5c6 Binary files /dev/null and b/apps/epic-react/public/assets/flying-rocket-light-sm@2x.webp differ diff --git a/apps/epic-react/public/assets/flying-rocket-light-xl@2x.webp b/apps/epic-react/public/assets/flying-rocket-light-xl@2x.webp new file mode 100644 index 000000000..2c1cd0423 Binary files /dev/null and b/apps/epic-react/public/assets/flying-rocket-light-xl@2x.webp differ diff --git a/apps/epic-react/public/assets/flying-rocket-light@2x.webp b/apps/epic-react/public/assets/flying-rocket-light@2x.webp new file mode 100644 index 000000000..8c43ff50c Binary files /dev/null and b/apps/epic-react/public/assets/flying-rocket-light@2x.webp differ diff --git a/apps/epic-react/public/assets/flying-rocket-sm@2x.webp b/apps/epic-react/public/assets/flying-rocket-sm@2x.webp new file mode 100644 index 000000000..4e4820209 Binary files /dev/null and b/apps/epic-react/public/assets/flying-rocket-sm@2x.webp differ diff --git a/apps/epic-react/public/assets/flying-rocket-xl@2x.webp b/apps/epic-react/public/assets/flying-rocket-xl@2x.webp new file mode 100644 index 000000000..ce3b9ecbd Binary files /dev/null and b/apps/epic-react/public/assets/flying-rocket-xl@2x.webp differ diff --git a/apps/epic-react/public/assets/flying-rocket@2x.webp b/apps/epic-react/public/assets/flying-rocket@2x.webp new file mode 100644 index 000000000..c604ff121 Binary files /dev/null and b/apps/epic-react/public/assets/flying-rocket@2x.webp differ diff --git a/apps/epic-react/public/assets/ring-planet-pattern@2x.jpg b/apps/epic-react/public/assets/ring-planet-pattern@2x.jpg new file mode 100644 index 000000000..f135ffdb3 Binary files /dev/null and b/apps/epic-react/public/assets/ring-planet-pattern@2x.jpg differ diff --git a/apps/epic-react/src/components/app/layout.tsx b/apps/epic-react/src/components/app/layout.tsx index d8f236211..05c05a7ac 100644 --- a/apps/epic-react/src/components/app/layout.tsx +++ b/apps/epic-react/src/components/app/layout.tsx @@ -9,6 +9,7 @@ const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-sans', + weight: ['400', '600', '800'], }) type LayoutProps = { @@ -81,7 +82,7 @@ const Layout: FunctionComponent> = ({ {withNavigation && }
diff --git a/apps/epic-react/src/components/app/navigation.tsx b/apps/epic-react/src/components/app/navigation.tsx deleted file mode 100644 index f13944cd4..000000000 --- a/apps/epic-react/src/components/app/navigation.tsx +++ /dev/null @@ -1,561 +0,0 @@ -import React from 'react' -import {useRouter} from 'next/router' -import Link from 'next/link' -import {track} from '@/utils/analytics' -import cx from 'classnames' -import { - AnimatePresence, - AnimationControls, - motion, - useAnimationControls, -} from 'framer-motion' -import {createAppAbility} from '@skillrecordings/skill-lesson/utils/ability' -import {trpc} from '@/trpc/trpc.client' -import Gravatar from 'react-gravatar' -import {signOut, useSession} from 'next-auth/react' -import {cn} from '@skillrecordings/ui/utils/cn' -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@skillrecordings/ui' -import {LogoutIcon} from '@heroicons/react/solid' -import {ChevronDownIcon} from '@heroicons/react/outline' -import Countdown, {zeroPad} from 'react-countdown' -import Image from 'next/image' -import Container from './container' -import {ThemeToggle} from './theme-toggle' -import common from '@/text/common' - -type NavigationProps = { - className?: string - navigationContainerClassName?: string - size?: 'sm' | 'md' | 'lg' -} - -const useAbilities = () => { - const {data: abilityRules} = trpc.abilities.getAbilities.useQuery() - - return createAppAbility(abilityRules || []) -} - -export const useNavigationLinks = () => { - const ability = useAbilities() - const canCreateContent = ability.can('create', 'Content') - - return [ - { - label: 'Workshops', - icon: () => '', - href: '/workshops', - }, - { - label: 'Articles', - icon: () => '', - href: '/articles', - }, - { - label: 'Podcast', - icon: () => '', - href: '/podcast/kents-career-path-through-web-development', - }, - ] -} - -const Navigation: React.FC = ({ - className, - size = 'md', - navigationContainerClassName, -}) => { - const {pathname, asPath, push} = useRouter() - const isRoot = pathname === '/' - const [menuOpen, setMenuOpen] = React.useState(false) - const navigationLinks = useNavigationLinks() - - const {data: commerceProps, status: commercePropsStatus} = - trpc.pricing.propsForCommerce.useQuery({ - productId: process.env.NEXT_PUBLIC_DEFAULT_PRODUCT_ID, - }) - - const {data: lastPurchase, status: lastPurchaseStatus} = - trpc.purchases.getLastPurchase.useQuery() - - const purchasedProductIds = - commerceProps?.purchases?.map((purchase) => purchase.productId) || [] - const hasPurchase = purchasedProductIds.length > 0 - const ability = useAbilities() - const canInviteTeam = ability.can('invite', 'Team') - const currentSale = useAvailableSale() - - return ( - <> - -
- - -
- { - event.preventDefault() - push('/brand') - }} - > - - -
- {navigationLinks.map(({label, href, icon}, i) => { - return ( - { - track(`clicked ${label} from navigation`, { - page: asPath, - }) - }} - > - {icon()} {label} - - ) - })} -
-
-
- - - {commercePropsStatus === 'success' && hasPurchase && ( - <> - {canInviteTeam && lastPurchase ? ( - - Invite Team - - ) : ( - - My Products - - )} - - )} - - -
- - {menuOpen && ( - - {navigationLinks.map(({label, href, icon}, i) => { - return ( - { - track(`clicked ${label} from navigation`, { - page: asPath, - }) - }} - > - {label} - - ) - })} -
- - - {commercePropsStatus === 'success' && - purchasedProductIds.length > 0 && ( - - My Products - - )} -
-
- )} -
-
-
-
- - ) -} - -export default Navigation - -type IconProps = { - isHovered: boolean - theme: 'light' | 'dark' -} - -const User: React.FC<{className?: string}> = ({className}) => { - const {pathname} = useRouter() - const {data: sessionData, status: sessionStatus} = useSession() - const {data: commerceProps, status: commercePropsStatus} = - trpc.pricing.propsForCommerce.useQuery({}) - const isLoadingUserInfo = - sessionStatus === 'loading' || commercePropsStatus === 'loading' - const purchasedProductIds = - commerceProps?.purchases?.map((purchase) => purchase.productId) || [] - const ability = useAbilities() - const canCreateContent = ability.can('create', 'Content') - - return ( - <> - {isLoadingUserInfo || !sessionData?.user?.email ? null : ( - - - -
- - - {sessionData?.user?.name?.split(' ')[0]} - {' '} - - -
-
- - - {sessionData?.user?.email || 'Account'} - - - - - Profile - - - {purchasedProductIds.length > 0 && ( - - - My Products - - - )} - {canCreateContent && ( - <> - {' '} - - - - Admin - - - - )} - - { - signOut() - }} - className="flex items-center justify-between" - > - {' '} - Log out - - - -
- )} - - ) -} - -const Login: React.FC<{className?: string}> = ({className}) => { - const {pathname} = useRouter() - const {data: sessionData, status: sessionStatus} = useSession() - const isLoadingUserInfo = sessionStatus === 'loading' - - return ( - <> - {isLoadingUserInfo || sessionData?.user?.email ? null : ( - - Log in - - )} - - ) -} - -export const HamburgerMenuIcon = () => { - return ( - - - - ) -} - -export const CrossIcon = () => { - return ( - - - - ) -} - -type NavToggleProps = { - isMenuOpened: boolean - setMenuOpened: (value: boolean) => void - menuControls?: AnimationControls -} - -const NavToggle: React.FC = ({ - isMenuOpened, - setMenuOpened, - menuControls, -}) => { - const path01Variants = { - open: {d: 'M3.06061 2.99999L21.0606 21'}, - closed: {d: 'M0 9.5L24 9.5'}, - } - const path02Variants = { - open: {d: 'M3.00006 21.0607L21 3.06064'}, - moving: {d: 'M0 14.5L24 14.5'}, - closed: {d: 'M0 14.5L15 14.5'}, - } - const path01Controls = useAnimationControls() - const path02Controls = useAnimationControls() - - return ( - - ) -} - -export const Logo: React.FC<{className?: string}> = ({className}) => { - return ( -
- {' '} - {process.env.NEXT_PUBLIC_SITE_TITLE} -
- ) -} - -export const SaleBanner: React.FC<{size?: 'sm' | 'md' | 'lg'}> = ({size}) => { - const currentSale = useAvailableSale() - - if (!currentSale) return null - - return ( -
- { - track('clicked sale banner cta', { - location: 'nav', - }) - }} - > -
-
- - Save {(Number(currentSale.percentageDiscount) * 100).toString()}% - on{' '} - {currentSale.product?.name || - process.env.NEXT_PUBLIC_PRODUCT_NAME} - - { - return ( -
- Price goes up in: - {days}d - {hours}h - {minutes}m - - {zeroPad(seconds)}s - -
- ) - }} - /> -
-
- {common['sale-banner-cta-label']} -
-
- -
- ) -} - -export const useAvailableSale = () => { - const {data} = trpc.pricing.defaultCoupon.useQuery() - const {data: commerceProps, status: commercePropsStatus} = - trpc.pricing.propsForCommerce.useQuery({ - productId: process.env.NEXT_PUBLIC_DEFAULT_PRODUCT_ID, - }) - - const purchasedProductIds = - commerceProps?.purchases?.map((purchase) => purchase.productId) || [] - const hasPurchase = purchasedProductIds.length > 0 - - if (!data) return null - if (hasPurchase) return null - - return {...data, bannerHeight: data ? 36 : 0} -} diff --git a/apps/epic-react/src/components/app/navigation/index.tsx b/apps/epic-react/src/components/app/navigation/index.tsx new file mode 100644 index 000000000..8dc4a721d --- /dev/null +++ b/apps/epic-react/src/components/app/navigation/index.tsx @@ -0,0 +1,342 @@ +import * as React from 'react' +import Link from 'next/link' +import {useRouter, usePathname} from 'next/navigation' +import {signOut, useSession} from 'next-auth/react' +import {motion, useScroll, useTransform, useSpring} from 'framer-motion' +import {useMedia} from 'react-use' +import {isEmpty} from 'lodash' +import cx from 'classnames' +import {twMerge} from 'tailwind-merge' +import { + Button, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@skillrecordings/ui' + +import {trpc} from '@/trpc/trpc.client' + +import MessageBar from '@/components/app/navigation/message-bar' +import Logo from '@/components/app/navigation/logo' +import {ThemeToggle} from '@/components/app/theme-toggle' +import {Message, Logout} from '@/components/icons' +import Skeleton from '@/components/skeleton' +import Feedback from '@/components/feedback' + +type NavigationProps = { + children?: React.ReactNode + className?: string +} + +const sellingLive = true + +const isTeamPurchaser = (user: any) => { + return ( + !isEmpty(user?.purchases) && + user.purchases.every((purchase: any) => { + return Boolean(purchase.bulkCouponId) + }) + ) +} + +const Navigation: React.FC = ({children}) => { + const {data: sessionData, status: sessionStatus} = useSession() + const [isOpen, setOpen] = React.useState(false) + const [hasMounted, setMounted] = React.useState(false) + const isAuthenticated = sessionStatus === 'authenticated' + const router = useRouter() + const location = usePathname() + const currentSale = useAvailableSale() + const isMobile = useMedia('(max-width: 640px)') + const isTablet = useMedia('(max-width: 920px)') + const isXL = useMedia('(min-width: 1200px)') + const {scrollY} = useScroll() + const messageBarTransform = useTransform( + scrollY, + [0, 200], + [0, isMobile ? -70 : -40], + ) + const dismissMessageBar = useSpring(messageBarTransform, { + stiffness: 300, + damping: 90, + }) + + // TODO: probably additional conditions required + const showDiscountBar = + currentSale?.percentageDiscount && + !location.includes('modules') && + isAuthenticated + const purchasedOnlyTeam = isTeamPurchaser(sessionData?.user) + + React.useLayoutEffect(() => { + setMounted(true) + }, []) + + const Links = ({isTablet = false}) => { + if (hasMounted) { + return ( + <> + {/* TODO: sellingLive? */} + {sellingLive && !isAuthenticated && isTablet ? ( + + ) : null} + + Articles + + + Livestreams + + + Podcast + + + FAQ + + {/* TODO: sellingLive? */} + {sellingLive && !isAuthenticated ? ( + + Restore Purchases + + ) : null} + + {isAuthenticated && + !isEmpty(sessionData?.user?.purchases) && + !purchasedOnlyTeam ? ( + + Workshops + + ) : null} +
+ {/* TODO: component */} + {isAuthenticated ? ( + {isTablet ? 'Send Feedback' : } + ) : null} + + + + {isAuthenticated ? ( + + + + + + {!isTablet && ( + +

Log Out

+
+ )} +
+
+ ) : null} +
+ + ) + } + return ( +
+ {!isTablet || !isMobile ? : null} + +
+ ) + } + + return hasMounted ? ( +
+ {showDiscountBar && ( + + + + )} + +
+ + + + + {/* TODO: reference: epic-react-gatsby-main/src/layouts/video-resource-page-layout.js */} + {children && isXL && ( + + {children} + + )} +
+ + {isTablet ? ( + <> + + {isOpen ? ( +
+ +
+ ) : null} + + ) : ( +
+ +
+ )} +
+
+ ) : null +} + +export default Navigation + +const MenuPath = (props: any) => ( + +) + +export const useAvailableSale = () => { + const {data} = trpc.pricing.defaultCoupon.useQuery() + const {data: commerceProps, status: commercePropsStatus} = + trpc.pricing.propsForCommerce.useQuery({ + productId: process.env.NEXT_PUBLIC_DEFAULT_PRODUCT_ID, + }) + + const purchasedProductIds = + commerceProps?.purchases?.map((purchase) => purchase.productId) || [] + const hasPurchase = purchasedProductIds.length > 0 + + if (!data) return null + if (hasPurchase) return null + + return {...data, bannerHeight: data ? 36 : 0} +} diff --git a/apps/epic-react/src/components/app/navigation/logo.tsx b/apps/epic-react/src/components/app/navigation/logo.tsx new file mode 100644 index 000000000..7560587df --- /dev/null +++ b/apps/epic-react/src/components/app/navigation/logo.tsx @@ -0,0 +1,66 @@ +import React from 'react' + +const Logo = ({isMobile}: {isMobile: boolean}) => { + return ( + <> + + + + + + + + + +
+
+ Epic React +
+
+ by Kent C. Dodds +
+
+ + ) +} + +export default Logo diff --git a/apps/epic-react/src/components/app/navigation/message-bar.tsx b/apps/epic-react/src/components/app/navigation/message-bar.tsx new file mode 100644 index 000000000..93ee2972c --- /dev/null +++ b/apps/epic-react/src/components/app/navigation/message-bar.tsx @@ -0,0 +1,54 @@ +'use client' + +import React from 'react' +import {motion} from 'framer-motion' +import {scroller} from 'react-scroll' +import {useRouter, usePathname} from 'next/navigation' + +const MessageBar: React.FC<{percentageDiscount: string}> = ({ + percentageDiscount, +}) => { + const router = useRouter() + const location = usePathname() + return ( +
+
+
+ {/* prettier-ignore */} + +
+ + Don't miss your chance to{' '} + + save{' '} + {percentageDiscount ? ( + percentageDiscount + ) : ( + + — + + )} + % + + ! + +
+ +
+ ) +} + +export default MessageBar diff --git a/apps/epic-react/src/components/app/theme-toggle.tsx b/apps/epic-react/src/components/app/theme-toggle.tsx index f2282c798..b72fb161a 100644 --- a/apps/epic-react/src/components/app/theme-toggle.tsx +++ b/apps/epic-react/src/components/app/theme-toggle.tsx @@ -1,39 +1,57 @@ 'use client' import * as React from 'react' -import {Moon, Sun} from 'lucide-react' +import {useMedia} from 'react-use' +import cx from 'classnames' +import {twMerge} from 'tailwind-merge' + +import {Sun, Moon} from '@/components/icons' import {useTheme} from 'next-themes' import { Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from '@skillrecordings/ui' export function ThemeToggle() { - const {setTheme} = useTheme() - + const {theme, setTheme} = useTheme() + const isTablet = useMedia('(max-width: 920px)') return ( - - - - - - setTheme('light')}> - Light - - setTheme('dark')}> - Dark - - setTheme('system')}> - System - - - + + + + + + +

Activate {theme === 'light' ? 'Dark' : 'Light'} Mode

+
+
+
) } diff --git a/apps/epic-react/src/components/divider.tsx b/apps/epic-react/src/components/divider.tsx index d3d6bf95d..87f269e1b 100644 --- a/apps/epic-react/src/components/divider.tsx +++ b/apps/epic-react/src/components/divider.tsx @@ -17,7 +17,7 @@ const Divider = ({className = ''}: {className?: string}) => ( duration: 1.5, ease: 'linear', }} - className="mx-auto w-24 text-[#9eddf8]" + className="mx-auto w-24 text-react" width="123" height="16" viewBox="0 0 123 16" diff --git a/apps/epic-react/src/components/feedback.tsx b/apps/epic-react/src/components/feedback.tsx new file mode 100644 index 000000000..f77060a22 --- /dev/null +++ b/apps/epic-react/src/components/feedback.tsx @@ -0,0 +1,429 @@ +import * as React from 'react' +import {useMedia} from 'react-use' +import cx from 'classnames' +import {twMerge} from 'tailwind-merge' + +import { + Button, + Input, + Label, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogPortal, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@skillrecordings/ui' + +const Feedback: React.FC<{children: React.ReactNode}> = ({children}) => { + const isTablet = useMedia('(max-width: 920px)') + return ( + + + {/* + + */} + + {/* + {!isTablet && ( + +

Send Feedback

+
+ )} +
+
*/} +
+ + + + Edit profile + + Make changes to your profile here. Click save when you're done. + + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ ) +} + +export default Feedback + +// import axios from '../../utils/axios' +// import * as Yup from 'yup' +// import Img from 'gatsby-image' +// import mq from '../../utils/mq' +// import Tippy from '@tippyjs/react' +// import isEmpty from 'lodash/isEmpty' +// import {motion} from 'framer-motion' +// import {useStaticQuery, graphql} from 'gatsby' +// import {useInterval, useMedia} from 'react-use' +// import {RemoveScroll} from 'react-remove-scroll' +// import {Formik, Form, Field, ErrorMessage} from 'formik' +// import {useEggheadUser} from '../../hooks/useEggheadUser' +// import {DialogOverlay, DialogContent} from '@reach/dialog' + +// import Sob from './images/Sob' +// import Hearteyes from './images/Hearteyes' +// import NeutralFace from './images/NeutralFace' + +// const feedbackSchema = Yup.object().shape({ +// emoji: Yup.string(), +// feedback: Yup.string().when('emoji', { +// is: undefined, +// then: Yup.string() +// .required( +// `Can't stay empty. Please either pick an emoji or write some feedback. 🙏`, +// ) +// .min(4, `Too short. Tell me more! 😊`), +// }), +// }) + +// const EMOJIS = new Map([ +// [, 'heart_eyes'], +// [, 'neutral_face'], +// [, 'sob'], +// ]) + +// const Feedback = ({className, children}) => { +// const {authToken, user} = useEggheadUser() +// const authHeaders = authToken +// ? { +// Authorization: `Bearer ${authToken()}`, +// } +// : {} + +// const isMobile = useMedia('(max-width: 640px)') +// const isTablet = useMedia('(max-width: 768px)') + +// const [showDialog, setShowDialog] = React.useState(false) +// const [state, setState] = React.useState({ +// loading: false, +// success: false, +// errorMessage: null, +// }) +// const openDialog = () => { +// setShowDialog(true) +// setState({success: false}) +// } +// const closeDialog = () => { +// setShowDialog(false) +// } + +// useInterval(() => closeDialog(), state.success ? 2650 : null) + +// function handleSubmit(values, actions) { +// const slackEmojiCode = isEmpty(values.emoji) +// ? ':unicorn_face:' +// : `:${values.emoji}:` + +// setState({loading: true}) +// actions.setSubmitting(true) +// axios +// .post( +// `${process.env.AUTH_DOMAIN}/api/v1/feedback`, +// { +// feedback: { +// url: window.location.toString(), +// site: process.env.SITE_NAME, +// comment: values.feedback, +// user: user, +// emotion: slackEmojiCode, +// }, +// }, +// {headers: authHeaders}, +// ) +// .then(() => { +// actions.setSubmitting(false) +// actions.resetForm() +// setState({ +// success: true, +// }) +// }) +// .catch((err) => { +// actions.setSubmitting(false) +// setState({success: false, errorMessage: err.message}) +// }) +// } + +// let EMOJI_CODES = null + +// function getEmoji(code) { +// if (code === null) return code +// if (EMOJI_CODES === null) { +// EMOJI_CODES = new Map([...EMOJIS].map(([k, v]) => [v, k])) +// } +// return EMOJI_CODES.get(code) +// } + +// const Emoji = ({code}) => getEmoji(code) + +// const image = useStaticQuery(graphql` +// query { +// kent: file(relativePath: {eq: "kent@2x.png"}) { +// childImageSharp { +// fluid(maxWidth: 48, quality: 100) { +// ...GatsbyImageSharpFluid_withWebp_tracedSVG +// } +// } +// } +// } +// `) + +// return ( +// <> +// +// Send Feedback +// +// ) +// } +// > +// +// +// +// +// +//
+// {state.success ? ( +// +// {/* prettier-ignore */} +//
+//

+// Thank you! +//

+//
+// ) : ( +// <> +//

+// Tell me how you feel about it +//

+// +// handleSubmit(values, actions) +// } +// > +// {({errors, isValid, touched, isSubmitting, values}) => { +// return ( +//
+//
+//
+// Pick an emoji +//
+//
+// {Array.from(EMOJIS.values()).map((emoji) => { +// return ( +// +// ) +// })} +//
+//
+ +// +// +//
+// ( +//
+//
+// +//
+//
+// {msg} +// {state.errorMessage && +// ` & ${state.errorMessage}`} +//
+//
+// )} +// /> +//
+// +//
+// +// ) +// }} +// +// +// )} +//
+//
+// +// {state.success && ( +// +// {/* prettier-ignore */} +// +// +// )} +//
+// +// +// +// +// ) +// } + +// export default Feedback diff --git a/apps/epic-react/src/components/icons/index.ts b/apps/epic-react/src/components/icons/index.ts new file mode 100644 index 000000000..b9e3cea50 --- /dev/null +++ b/apps/epic-react/src/components/icons/index.ts @@ -0,0 +1,6 @@ +import Sun from './sun' +import Moon from './moon' +import Logout from './logout' +import Message from './message' + +export {Sun, Moon, Logout, Message} diff --git a/apps/epic-react/src/components/icons/logout.tsx b/apps/epic-react/src/components/icons/logout.tsx new file mode 100644 index 000000000..94c2fdf8d --- /dev/null +++ b/apps/epic-react/src/components/icons/logout.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' + +export default function Logout() { + return ( + + + + + ) +} diff --git a/apps/epic-react/src/components/icons/message.tsx b/apps/epic-react/src/components/icons/message.tsx new file mode 100644 index 000000000..9ad23dc25 --- /dev/null +++ b/apps/epic-react/src/components/icons/message.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +export default function Message() { + return ( + + + + ) +} diff --git a/apps/epic-react/src/components/icons/moon.tsx b/apps/epic-react/src/components/icons/moon.tsx new file mode 100644 index 000000000..1b90b4ea9 --- /dev/null +++ b/apps/epic-react/src/components/icons/moon.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +export default function Moon() { + return ( + + + + ) +} diff --git a/apps/epic-react/src/components/icons/sun.tsx b/apps/epic-react/src/components/icons/sun.tsx new file mode 100644 index 000000000..343ac4252 --- /dev/null +++ b/apps/epic-react/src/components/icons/sun.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' + +export default function Sun() { + return ( + + + + + + + + + + + + ) +} diff --git a/apps/epic-react/src/components/skeleton.tsx b/apps/epic-react/src/components/skeleton.tsx new file mode 100644 index 000000000..e45590cdf --- /dev/null +++ b/apps/epic-react/src/components/skeleton.tsx @@ -0,0 +1,22 @@ +import {motion} from 'framer-motion' + +export default function Skeleton(props: any) { + return ( + + + + ) +} diff --git a/apps/epic-react/src/pages/404.tsx b/apps/epic-react/src/pages/404.tsx index 57526c8c6..8c7baec80 100644 --- a/apps/epic-react/src/pages/404.tsx +++ b/apps/epic-react/src/pages/404.tsx @@ -21,8 +21,8 @@ export default function Custom404() { height={700} />
-
404
-

Not Found

+
404
+

Not Found

diff --git a/apps/epic-react/src/pages/learn.tsx b/apps/epic-react/src/pages/learn.tsx index eb034601e..7a07f9697 100644 --- a/apps/epic-react/src/pages/learn.tsx +++ b/apps/epic-react/src/pages/learn.tsx @@ -147,7 +147,50 @@ const Learn: React.FC<{workshops: any[]; bonuses: any[]}> = ({ }, }} > -
+
+ + + + + + +

Learn Page

    diff --git a/apps/epic-react/src/pages/login.tsx b/apps/epic-react/src/pages/login.tsx index 79135fe48..4b4cfe4a7 100644 --- a/apps/epic-react/src/pages/login.tsx +++ b/apps/epic-react/src/pages/login.tsx @@ -1,4 +1,5 @@ import React from 'react' +import Image from 'next/image' import Layout from '@/components/app/layout' import {GetServerSideProps} from 'next' import {getCsrfToken, getProviders} from 'next-auth/react' @@ -20,12 +21,25 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const LoginPage: React.FC = ({csrfToken, providers}) => { return ( - - + a lost cosmonaut +
    +
    + +
    +
    ) } diff --git a/apps/epic-react/src/styles/globals.css b/apps/epic-react/src/styles/globals.css index aeb720dfc..c9c2af94a 100644 --- a/apps/epic-react/src/styles/globals.css +++ b/apps/epic-react/src/styles/globals.css @@ -27,66 +27,80 @@ :root { --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - --popover: 0 0% 100%; --popover-foreground: 222.2 47.4% 11.2%; - --border: 214.3 31.8% 94.4%; --input: 214.3 31.8% 94.4%; - --card: 0 0% 100%; --card-foreground: 222.2 47.4% 11.2%; - --primary: 222 47% 11%; --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; - --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 100% 50%; --destructive-foreground: 210 40% 98%; - --ring: 215 20.2% 65.1%; - --radius: 0.5rem; + + --color-text-primary: #225feb; + --color-text-text: #212732; + --color-text-white: #ffffff; + --color-bg-background: #ffffff; + --color-bg-navigation: rgba(255, 255, 255, 0.9); + --color-bg-black: #0e182a; + --color-gray-100: #f0f2f7; + --color-gray-200: #e2e7ed; + --color-gray-300: #d8dee6; + --color-gray-400: #c3ced8; + --color-gray-500: #a0aec0; + --color-gray-600: #718096; + --color-gray-700: #4a5568; + --color-gray-800: #293448; + --color-gray-900: #1a202c; + --color-react: #1e75d9; } .dark { --background: 219, 50%, 11%; --foreground: 213 31% 91%; - --muted: 223 47% 11%; --muted-foreground: 215.4 16.3% 56.9%; - --accent: 216 34% 17%; --accent-foreground: 210 40% 98%; - --popover: 224 71% 4%; --popover-foreground: 215 20.2% 65.1%; - --border: 216 34% 17%; --input: 216 34% 17%; - --card: 224 71% 4%; --card-foreground: 213 31% 91%; - --primary: 222 47% 11%; --primary-foreground: 222.2 47.4% 1.2%; - --secondary: 222.2 47.4% 11.2%; --secondary-foreground: 210 40% 98%; - --destructive: 0 63% 31%; --destructive-foreground: 210 40% 98%; - --ring: 216 34% 17%; - --radius: 0.5rem; + + --color-text-primary: #81a7ff; + --color-text-text: #ffffff; + --color-text-white: #ffffff; + --color-bg-background: #0e182a; + --color-bg-navigation: rgba(14, 24, 42, 0.9); + --color-bg-black: #0e182a; + --color-gray-100: #132035; + --color-gray-200: #222f44; + --color-gray-300: #384357; + --color-gray-400: #718096; + --color-gray-500: #a0aec0; + --color-gray-600: #cbd5e0; + --color-gray-700: #e2e8f0; + --color-gray-800: #edf2f7; + --color-gray-900: #f7fafc; + --color-react: #9eddf8; } } @@ -98,4 +112,195 @@ @apply bg-background text-foreground; font-feature-settings: 'rlig' 1, 'calt' 1; } + .container { + @apply mx-auto max-w-screen-md p-5 text-text; + } + .btn-primary { + @apply inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-3 py-2 text-sm font-semibold leading-6 text-white transition duration-150 ease-in-out; + color: white !important; + @screen md { + @apply px-4 py-3 text-base; + } + } + .btn-primary:hover { + @apply bg-indigo-500; + text-decoration: none !important; + } + .btn-primary:focus { + @apply shadow-outline-indigo border-indigo-700 outline-none; + } + .btn-primary:active { + @apply bg-indigo-700; + } + + .btn-secondary { + @apply inline-flex items-center rounded-md border border-transparent bg-gray-200 px-3 py-2 text-sm font-semibold leading-6 text-white transition duration-150 ease-in-out; + color: white !important; + @screen md { + @apply px-4 py-3 text-base; + } + } + .btn-secondary:hover { + @apply bg-indigo-600; + text-decoration: none !important; + } + .btn-secondary:focus { + @apply shadow-outline-indigo border-indigo-700 outline-none; + } + .btn-secondary:active { + @apply bg-indigo-700; + } + + ::selection { + background: #3371ff; + color: white; + } + + .autolink-header { + @apply absolute -ml-5 inline-block; + + svg { + fill: var(--color-gray-300); + } + svg:hover { + fill: var(--color-gray-800); + } + } + + .font-numeric { + font-variant-numeric: tabular-nums; + } + + /* + This will hide the focus indicator if the element receives focus via the mouse, + but it will still show up on keyboard focus. +*/ + .js-focus-visible :focus:not(.focus-visible) { + outline: none; + } + + /* Links */ + + .links a { + @apply text-primary; + } + + .links a:hover { + @apply underline; + } + + /* Navigation */ + + .navigation { + @apply bg-navigation; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + } + + /* purgecss start ignore */ + + /* Markdown Styles */ + /* Global */ + .markdown { + @apply text-sm leading-relaxed text-text; + @screen sm { + @apply text-lg; + } + } + /* Headers */ + .markdown h1 { + @screen md { + @apply text-3xl; + } + @apply my-8 flex items-center text-2xl font-bold leading-tight; + overflow-wrap: anywhere; + word-wrap: anywhere; + } + + .markdown h2 { + @screen md { + @apply text-2xl; + } + @apply my-6 flex items-center text-xl font-bold leading-tight; + overflow-wrap: anywhere; + word-wrap: anywhere; + } + + .markdown h3, + .markdown h4, + .markdown h5, + .markdown h6 { + @screen md { + @apply text-xl; + } + @apply my-4 flex items-center text-lg font-semibold leading-tight; + overflow-wrap: anywhere; + word-wrap: anywhere; + } + /* Links */ + .markdown a { + @apply text-primary; + } + .markdown a:hover { + @apply underline; + } + /* Paragraph */ + .markdown p { + @apply mb-5; + } + /* Lists */ + .markdown ul, + .markdown ol { + @apply mb-4 ml-8; + } + .markdown li > p, + .markdown li > ul, + .markdown li > ol { + @apply mb-0; + } + .markdown ol { + @apply list-decimal; + } + .markdown ul { + @apply list-disc; + } + /* Blockquotes */ + .markdown blockquote { + @apply mx-6 mb-4 border-l-4 border-gray-400 bg-gray-100 p-2 italic; + } + .markdown blockquote > p { + @apply mb-0; + } + /* Tables */ + .markdown td, + .markdown th { + @apply border border-gray-400 px-2 py-1; + } + .markdown tr:nth-child(odd) { + @apply bg-gray-100; + } + .markdown table { + @apply mb-6; + } + + /* Code in titles */ + .markdown code { + @apply border-b-2 border-gray-200; + font-size: 90%; + } + + /* Code in paragraphs */ + .markdown p > code { + @apply rounded-sm border-transparent bg-gray-200 text-sm font-semibold; + padding: 3px 4px 1px 4px; + } + + .markdown hr { + @apply my-8; + } + + /* iframe */ + .markdown iframe { + margin-bottom: 1.25rem; + } } diff --git a/apps/epic-react/src/styles/login.css b/apps/epic-react/src/styles/login.css index 0ac6e340c..e3a8d47be 100644 --- a/apps/epic-react/src/styles/login.css +++ b/apps/epic-react/src/styles/login.css @@ -1,37 +1,24 @@ [data-login-template] { - @apply relative mx-auto flex w-full max-w-sm flex-grow flex-col items-center justify-start pb-16 pt-24 sm:p-5 sm:pt-32; + @apply rounded-lg border border-er-gray-200 bg-background p-8 shadow-xl sm:mx-auto sm:w-full sm:max-w-md; [data-title] { - @apply pt-3 text-center text-4xl font-extrabold leading-9 sm:pt-8 sm:text-4xl; - } - [data-verification-error] { - @apply max-w-sm pt-4 text-center sm:mx-auto sm:w-full sm:pt-8; + @apply mt-6 text-center text-3xl font-semibold leading-9 text-er-gray-900; } [data-form] { - @apply pt-8 sm:mx-auto sm:w-full sm:max-w-md sm:pt-10 sm:text-lg; - + @apply mt-8 px-4 pb-8 sm:mx-auto sm:w-full sm:max-w-md sm:px-8; + [data-label] { + @apply block text-sm font-medium leading-5 text-er-gray-800; + } [data-input-container] { @apply relative mt-1 rounded-md shadow-sm; [data-icon] { - @apply pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3; - svg { - @apply h-5 w-5; - } + @apply hidden; } [data-input] { - @apply pl-10; + @apply block w-full appearance-none rounded-md border-2 border-er-gray-200 bg-er-gray-100 px-4 py-2 leading-normal focus:shadow-outline focus:outline-none; } } [data-button] { - @apply mt-4 w-full transition hover:saturate-200; - } - } - [data-separator] { - @apply py-5 text-center text-sm opacity-60; - } - [data-providers-container] { - @apply w-full space-y-2; - [data-button] { - @apply w-full; + @apply mt-6 w-full rounded-lg bg-blue-500 px-5 py-3 font-semibold text-white shadow-sm transition duration-150 ease-in-out hover:bg-blue-700; } } } diff --git a/apps/epic-react/tailwind.config.js b/apps/epic-react/tailwind.config.js index aebe17d41..2f021e4bf 100644 --- a/apps/epic-react/tailwind.config.js +++ b/apps/epic-react/tailwind.config.js @@ -18,19 +18,47 @@ module.exports = { './node_modules/@skillrecordings/skill-lesson/video/**/*.tsx', ], theme: { - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1280px', - }, - }, extend: { screens: { '2xl': '1820px', }, + textColor: { + text: 'var(--color-text-text)', + 'er-primary': 'var(--color-text-primary)', + react: 'var(--color-react)', + }, + backgroundColor: { + background: 'var(--color-bg-background)', + navigation: 'var(--color-bg-navigation)', + black: 'var(--color-bg-black)', + }, + boxShadow: { + outline: '0 0 0 3px var(--color-text-primary)', + }, colors: { gray: colors.slate, + 'er-gray': { + 100: 'var(--color-gray-100)', + 200: 'var(--color-gray-200)', + 300: 'var(--color-gray-300)', + 400: 'var(--color-gray-400)', + 500: 'var(--color-gray-500)', + 600: 'var(--color-gray-600)', + 700: 'var(--color-gray-700)', + 800: 'var(--color-gray-800)', + 900: 'var(--color-gray-900)', + }, + blue: { + 100: '#EBF1FF', + 200: '#CCDCFF', + 300: '#ADC6FF', + 400: '#709CFF', + 500: '#3371FF', + 600: '#2E66E6', + 700: '#1F4499', + 800: '#173373', + 900: '#0F224D', + }, border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', @@ -88,8 +116,21 @@ module.exports = { 'accordion-up': 'accordion-up 0.2s ease-out', }, typography: (theme) => ({ - DEFAULT: { - css: {}, + default: { + css: { + color: 'var(--color-gray-900)', + 'h1, h2, h3, h4, blockquote, i, em, strong': { + color: 'var(--color-text-text)', + }, + a: {color: 'var(--color-text-primary)'}, + code: { + color: 'var(--color-text)', + padding: '2px 3px', + background: 'var(--color-gray-200)', + borderRadius: 3, + }, + pre: {margin: '0px', padding: '0px'}, + }, }, }), }, diff --git a/packages/ui/primitives/index.tsx b/packages/ui/primitives/index.tsx index 8c2d5645b..4639a409e 100644 --- a/packages/ui/primitives/index.tsx +++ b/packages/ui/primitives/index.tsx @@ -94,8 +94,10 @@ import { DialogContent, DialogDescription, DialogHeader, + DialogFooter, DialogTitle, DialogTrigger, + DialogPortal, } from './dialog' export { @@ -183,6 +185,8 @@ export { DialogContent, DialogDescription, DialogHeader, + DialogFooter, DialogTitle, DialogTrigger, + DialogPortal, }