Migrate the root layout from Vercel. (#2)

pull/3/head
Joshua Potter 2023-12-03 15:54:18 -07:00 committed by GitHub
parent ccc8423e0f
commit a1dd46561d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2892 additions and 165 deletions

View File

@ -1,24 +1,32 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
mixFiles=$( STAGED=$(
git --no-pager diff --name-status --no-color --cached | \ git --no-pager diff --name-only --no-color --cached --diff-filter=d |
awk '$1 != "D" && $2 ~ /\.exs?$/ {print $NF}' # Remove quotations used to surrounding filenames with special characters.
sed -e "s/^\"//" -e "s/\"$//g"
) )
for path in $mixFiles MIX_TARGETS=()
WEB_TARGETS=()
while IFS= read -r FILENAME
do do
mix format "$path" if [[ "$FILENAME" =~ .*\.exs? ]]; then
git add "$path" MIX_TARGETS+=("${FILENAME}")
done elif
[[ "$FILENAME" =~ assets/.*\.jsx? ]] ||
[[ "$FILENAME" =~ assets/.*\.tsx? ]]; then
WEB_TARGETS+=("${FILENAME#"assets/"}")
fi
done <<< "$STAGED"
webFiles=$( if (( ${#MIX_TARGETS[@]} )); then
git --no-pager diff --name-status --no-color --cached | \ mix format "${MIX_TARGETS[@]}"
awk '$1 != "D" && $2 ~ /\.jsx?$|\.tsx?$/ {print $NF}' git add "${MIX_TARGETS[@]}"
) fi
for path in $webFiles if (( ${#WEB_TARGETS[@]} )); then
do cd assets
prettier --write "$path" npx prettier --write "${WEB_TARGETS[@]}"
git add "$path" git add "${WEB_TARGETS[@]}"
done fi

View File

@ -14,16 +14,19 @@ html {
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
touch-action: manipulation; touch-action: manipulation;
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0; font-feature-settings:
"case" 1,
"rlig" 1,
"calt" 0;
} }
html, html,
body { body {
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue', font-family: -apple-system, system-ui, BlinkMacSystemFont, "Helvetica Neue",
'Helvetica', sans-serif; "Helvetica", sans-serif;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@apply text-white bg-white antialiased; @apply bg-white text-white antialiased;
} }
body { body {

View File

@ -22,13 +22,17 @@ import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view" import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let csrfToken = document
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } }) .querySelector("meta[name='csrf-token']")
.getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken },
})
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }) topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide())
// connect if there are any LiveViews on the page // connect if there are any LiveViews on the page
liveSocket.connect() liveSocket.connect()
@ -38,4 +42,3 @@ liveSocket.connect()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket

View File

@ -1,25 +1,13 @@
import * as React from "react"; import * as React from "react"
import { RouterProvider, createBrowserRouter } from "react-router-dom"; import { RouterProvider } from "react-router-dom"
import { Footer } from "./components/Footer";
const router = createBrowserRouter([ import { RootLayout } from "./components/RootLayout"
{ import { router } from "./router"
path: "/",
element: <Footer />, // Placeholder.
},
{
path: "/nested",
element: <Footer />, // Placeholder.
},
]);
export default function App() { export default function App() {
return ( return (
<div> <RootLayout>
<main className="w-full flex-auto">
<RouterProvider router={router} /> <RouterProvider router={router} />
</main> </RootLayout>
<Footer /> )
</div>
);
} }

View File

@ -1,11 +1,11 @@
import * as React from "react"; import * as React from "react"
import clsx from "clsx"; import clsx from "clsx"
type ContainerProps<T extends React.ElementType> = { type ContainerProps<T extends React.ElementType> = {
as?: T; as?: T
className?: string; className?: string
children: React.ReactNode; children: React.ReactNode
}; }
export function Container<T extends React.ElementType = "div">({ export function Container<T extends React.ElementType = "div">({
as, as,
@ -13,11 +13,11 @@ export function Container<T extends React.ElementType = "div">({
children, children,
}: Omit<React.ComponentPropsWithoutRef<T>, keyof ContainerProps<T>> & }: Omit<React.ComponentPropsWithoutRef<T>, keyof ContainerProps<T>> &
ContainerProps<T>) { ContainerProps<T>) {
let Component = as ?? "div"; let Component = as ?? "div"
return ( return (
<Component className={clsx("mx-auto max-w-7xl px-6 lg:px-8", className)}> <Component className={clsx("mx-auto max-w-7xl px-6 lg:px-8", className)}>
<div className="mx-auto max-w-2xl lg:max-w-none">{children}</div> <div className="mx-auto max-w-2xl lg:max-w-none">{children}</div>
</Component> </Component>
); )
} }

View File

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react"
import { Container } from "./Container"; import { Container } from "./Container"
import { Logo } from "./Logo"; import { Logo } from "./Logo"
const navigation = [ const navigation = [
{ {
@ -15,7 +15,7 @@ const navigation = [
{ title: "Contact Us", href: "/contact/" }, { title: "Contact Us", href: "/contact/" },
], ],
}, },
]; ]
function Navigation() { function Navigation() {
return ( return (
@ -42,7 +42,7 @@ function Navigation() {
))} ))}
</ul> </ul>
</nav> </nav>
); )
} }
export function Footer() { export function Footer() {
@ -60,5 +60,5 @@ export function Footer() {
</p> </p>
</div> </div>
</Container> </Container>
); )
} }

View File

@ -0,0 +1,127 @@
import * as React from "react"
import { motion } from "framer-motion"
function Block({
x,
y,
...props
}: Omit<React.ComponentPropsWithoutRef<typeof motion.path>, "x" | "y"> & {
x: number
y: number
}) {
return (
<motion.path
transform={`translate(${-32 * y + 96 * x} ${160 * y})`}
d="M45.119 4.5a11.5 11.5 0 0 0-11.277 9.245l-25.6 128C6.82 148.861 12.262 155.5 19.52 155.5h63.366a11.5 11.5 0 0 0 11.277-9.245l25.6-128c1.423-7.116-4.02-13.755-11.277-13.755H45.119Z"
{...props}
/>
)
}
export function GridPattern({
yOffset = 0,
interactive = false,
...props
}: React.ComponentPropsWithoutRef<"svg"> & {
yOffset?: number
interactive?: boolean
}) {
let id = React.useId()
let ref = React.useRef<React.ElementRef<"svg">>(null)
let currentBlock = React.useRef<[x: number, y: number]>()
let counter = React.useRef(0)
let [hoveredBlocks, setHoveredBlocks] = React.useState<
Array<[x: number, y: number, key: number]>
>([])
let staticBlocks = [
[1, 1],
[2, 2],
[4, 3],
[6, 2],
[7, 4],
[5, 5],
]
React.useEffect(() => {
if (!interactive) {
return
}
function onMouseMove(event: MouseEvent) {
if (!ref.current) {
return
}
let rect = ref.current.getBoundingClientRect()
let x = event.clientX - rect.left
let y = event.clientY - rect.top
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
return
}
x = x - rect.width / 2 - 32
y = y - yOffset
x += Math.tan(32 / 160) * y
x = Math.floor(x / 96)
y = Math.floor(y / 160)
if (currentBlock.current?.[0] === x && currentBlock.current?.[1] === y) {
return
}
currentBlock.current = [x, y]
setHoveredBlocks((blocks) => {
let key = counter.current++
let block = [x, y, key] as (typeof hoveredBlocks)[number]
return [...blocks, block].filter(
(block) => !(block[0] === x && block[1] === y && block[2] !== key)
)
})
}
window.addEventListener("mousemove", onMouseMove)
return () => {
window.removeEventListener("mousemove", onMouseMove)
}
}, [yOffset, interactive])
return (
<svg ref={ref} aria-hidden="true" {...props}>
<rect width="100%" height="100%" fill={`url(#${id})`} strokeWidth="0" />
<svg x="50%" y={yOffset} strokeWidth="0" className="overflow-visible">
{staticBlocks.map((block) => (
<Block key={`${block}`} x={block[0]} y={block[1]} />
))}
{hoveredBlocks.map((block) => (
<Block
key={block[2]}
x={block[0]}
y={block[1]}
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 1, times: [0, 0, 1] }}
onAnimationComplete={() => {
setHoveredBlocks((blocks) =>
blocks.filter((b) => b[2] !== block[2])
)
}}
/>
))}
</svg>
<defs>
<pattern
id={id}
width="96"
height="480"
x="50%"
patternUnits="userSpaceOnUse"
patternTransform={`translate(0 ${yOffset})`}
fill="none"
>
<path d="M128 0 98.572 147.138A16 16 0 0 1 82.883 160H13.117a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-45.117 320H-116M64-160 34.572-12.862A16 16 0 0 1 18.883 0h-69.766a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-109.117 160H-180M192 160l-29.428 147.138A15.999 15.999 0 0 1 146.883 320H77.117a16 16 0 0 0-15.69 12.862L34.573 467.138A16 16 0 0 1 18.883 480H-52M-136 480h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1-18.883 320h69.766a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 109.117 160H192M-72 640h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 45.117 480h69.766a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A15.999 15.999 0 0 1 173.117 320H256M-200 320h58.883a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A16 16 0 0 1-82.883 160h69.766a16 16 0 0 0 15.69-12.862L29.427 12.862A16 16 0 0 1 45.117 0H128" />
</pattern>
</defs>
</svg>
)
}

View File

@ -0,0 +1,57 @@
import * as React from "react"
import clsx from "clsx"
import MenuIcon from "../icons/Menu"
import XIcon from "../icons/X"
import { Container } from "./Container"
import { Logo } from "./Logo"
type HeaderProps = {
panelId: string
expanded: boolean
onToggle: () => void
toggleRef: React.RefObject<HTMLButtonElement>
invert?: boolean
}
export function Header({
panelId,
expanded,
onToggle,
toggleRef,
invert = false,
}: HeaderProps) {
const Icon = expanded ? XIcon : MenuIcon
const iconClasses = clsx(
"h-6 w-6",
invert
? "fill-white group-hover:fill-neutral-200"
: "fill-neutral-950 group-hover:fill-neutral-700"
)
return (
<Container>
<div className="flex items-center justify-between">
<a href="/" aria-label="Home">
<Logo className="h-8" invert={invert} />
</a>
<div className="flex items-center gap-x-8">
<button
ref={toggleRef}
type="button"
onClick={onToggle}
aria-expanded={expanded ? "true" : "false"}
aria-controls={panelId}
className={clsx(
"group -m-2.5 rounded-full p-2.5 transition",
invert ? "hover:bg-white/10" : "hover:bg-neutral-950/10"
)}
aria-label="Toggle navigation"
>
<Icon className={iconClasses} />
</button>
</div>
</div>
</Container>
)
}

View File

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react"
import clsx from "clsx"; import clsx from "clsx"
import LogoMark from "../icons/Logomark"; import LogoMark from "../icons/Logomark"
export function Logo({ export function Logo({
invert = false, invert = false,
@ -15,11 +15,11 @@ export function Logo({
<p <p
className={clsx( className={clsx(
"font-display text-xl font-bold tracking-tight", "font-display text-xl font-bold tracking-tight",
invert ? "text-white" : "text-neutral-950", invert ? "text-white" : "text-neutral-950"
)} )}
> >
BoardWise BoardWise
</p> </p>
</div> </div>
); )
} }

View File

@ -0,0 +1,139 @@
import * as React from "react"
import clsx from "clsx"
import { motion, MotionConfig, useReducedMotion } from "framer-motion"
import { Container } from "./Container"
import { Footer } from "./Footer"
import { GridPattern } from "./GridPattern"
import { Header } from "./Header"
function NavigationRow({ children }: { children: React.ReactNode }) {
return (
<div className="even:mt-px sm:bg-neutral-950">
<Container>
<div className="grid grid-cols-1 sm:grid-cols-2">{children}</div>
</Container>
</div>
)
}
function NavigationItem({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
return (
<a
href={href}
className="group relative isolate -mx-6 bg-neutral-950 px-6 py-10 even:mt-px sm:mx-0 sm:px-0 sm:py-16 sm:odd:pr-16 sm:even:mt-0 sm:even:border-l sm:even:border-neutral-800 sm:even:pl-16"
>
{children}
<span className="absolute inset-y-0 -z-10 w-screen bg-neutral-900 opacity-0 transition group-odd:right-0 group-even:left-0 group-hover:opacity-100" />
</a>
)
}
function Navigation() {
return (
<nav className="mt-px font-display text-5xl font-medium tracking-tight text-white">
<NavigationRow>
<NavigationItem href="/about/">About Us</NavigationItem>
</NavigationRow>
<NavigationRow>
<NavigationItem href="/contact/">Contact Us</NavigationItem>
</NavigationRow>
</nav>
)
}
export function RootLayout({ children }: { children: React.ReactNode }) {
let panelId = React.useId()
let [expanded, setExpanded] = React.useState(false)
let openRef = React.useRef<React.ElementRef<"button">>(null)
let closeRef = React.useRef<React.ElementRef<"button">>(null)
let shouldReduceMotion = useReducedMotion()
return (
<>
<MotionConfig
transition={{
ease: "easeInOut",
duration: shouldReduceMotion ? 0 : undefined,
}}
>
<header>
<div
className="absolute left-0 right-0 top-2 z-40 pt-6"
aria-hidden={expanded ? "true" : undefined}
// @ts-ignore (https://github.com/facebook/react/issues/17157)
inert={expanded ? "" : undefined}
>
<Header
panelId={panelId}
toggleRef={openRef}
expanded={expanded}
onToggle={() => {
setExpanded((expanded) => !expanded)
window.setTimeout(
() => closeRef.current?.focus({ preventScroll: true })
)
}}
/>
</div>
<motion.div
layout
id={panelId}
style={{ height: expanded ? "auto" : "1px" }}
className="relative z-50 overflow-hidden bg-neutral-950"
aria-hidden={expanded ? undefined : "true"}
// @ts-ignore (https://github.com/facebook/react/issues/17157)
inert={expanded ? undefined : ""}
>
<div className="bg-neutral-800">
<div className="bg-neutral-950 py-8">
<Header
invert
panelId={panelId}
toggleRef={closeRef}
expanded={expanded}
onToggle={() => {
setExpanded((expanded) => !expanded)
window.setTimeout(
() => openRef.current?.focus({ preventScroll: true })
)
}}
/>
</div>
<Navigation />
</div>
</motion.div>
</header>
</MotionConfig>
<div
className={clsx("relative flex flex-auto overflow-hidden bg-white", {
"pt-14": !expanded,
})}
>
<div
className={clsx("relative isolate flex w-full flex-col", {
"pt-9": !expanded,
})}
>
<GridPattern
className="absolute inset-x-0 -top-14 -z-10 h-[1000px] w-full fill-neutral-50 stroke-neutral-950/5 [mask-image:linear-gradient(to_bottom_left,white_40%,transparent_50%)]"
yOffset={-96}
interactive
/>
<main className="w-full flex-auto">{children}</main>
<Footer />
</div>
</div>
</>
)
}

View File

@ -1,22 +1,22 @@
import * as React from "react"; import * as React from "react"
const SvgComponent = ({ invert = false, size = 25, ...props }) => { const SvgComponent = ({ invert = false, size = 25, ...props }) => {
const color = invert ? "rgb(255, 255, 255)" : "rgb(10 10 10)"; const color = invert ? "rgb(255, 255, 255)" : "rgb(10 10 10)"
const radius = 5; const radius = 5
const roundedTopRightPath = ` const roundedTopRightPath = `
M ${size / 2} 0 M ${size / 2} 0
H ${size - radius} H ${size - radius}
Q ${size} 0, ${size} ${radius} Q ${size} 0, ${size} ${radius}
V ${size / 2} V ${size / 2}
H ${size / 2} H ${size / 2}
Z`; Z`
const roundedBottomLeftPath = ` const roundedBottomLeftPath = `
M 0 ${size - radius} M 0 ${size - radius}
Q 0 ${size}, ${radius} ${size} Q 0 ${size}, ${radius} ${size}
H ${size / 2} H ${size / 2}
V ${size / 2} V ${size / 2}
H 0 H 0
Z`; Z`
return ( return (
<svg <svg
@ -28,6 +28,7 @@ const SvgComponent = ({ invert = false, size = 25, ...props }) => {
<path d={roundedTopRightPath} fill={color} /> <path d={roundedTopRightPath} fill={color} />
<path d={roundedBottomLeftPath} fill={color} /> <path d={roundedBottomLeftPath} fill={color} />
</svg> </svg>
); )
}; }
export default SvgComponent;
export default SvgComponent

View File

@ -0,0 +1,14 @@
import * as React from "react"
const SvgComponent = ({ ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path d="M2 6h20v2H2zM2 16h20v2H2z" />
</svg>
)
export default SvgComponent

View File

@ -0,0 +1,9 @@
import * as React from "react"
const SvgComponent = ({ ...props }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1920" {...props}>
<path d="M797.32 985.882 344.772 1438.43l188.561 188.562 452.549-452.549 452.548 452.549 188.562-188.562-452.549-452.548 452.549-452.549-188.562-188.561L985.882 797.32 533.333 344.772 344.772 533.333z" />
</svg>
)
export default SvgComponent

View File

@ -1,6 +1,6 @@
import * as React from 'react' import * as React from "react"
import ReactDOM from 'react-dom/client' import ReactDOM from "react-dom/client"
import App from './App' import App from "./App"
ReactDOM.createRoot(document.getElementById('mount')!).render(<App />) ReactDOM.createRoot(document.getElementById("mount")!).render(<App />)

View File

@ -0,0 +1,9 @@
import * as React from "react"
import { createBrowserRouter } from "react-router-dom"
export const router = createBrowserRouter([
{
path: "/",
element: <div />,
},
])

View File

@ -4,6 +4,24 @@
let let
sources = { sources = {
"@emotion/is-prop-valid-0.8.8" = {
name = "_at_emotion_slash_is-prop-valid";
packageName = "@emotion/is-prop-valid";
version = "0.8.8";
src = fetchurl {
url = "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz";
sha512 = "u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==";
};
};
"@emotion/memoize-0.7.4" = {
name = "_at_emotion_slash_memoize";
packageName = "@emotion/memoize";
version = "0.7.4";
src = fetchurl {
url = "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz";
sha512 = "Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==";
};
};
"@remix-run/router-1.13.1" = { "@remix-run/router-1.13.1" = {
name = "_at_remix-run_slash_router"; name = "_at_remix-run_slash_router";
packageName = "@remix-run/router"; packageName = "@remix-run/router";
@ -22,6 +40,15 @@ let
sha512 = "rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="; sha512 = "rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==";
}; };
}; };
"framer-motion-10.16.12" = {
name = "framer-motion";
packageName = "framer-motion";
version = "10.16.12";
src = fetchurl {
url = "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.12.tgz";
sha512 = "w7Yzx0OzQ5Uh6uNkxaX+4TuAPuOKz3haSbjmHpdrqDpGuCJCpq6YP9Dy7JJWdZ6mJjndrg3Ao3vUwDajKNikCA==";
};
};
"js-tokens-4.0.0" = { "js-tokens-4.0.0" = {
name = "js-tokens"; name = "js-tokens";
packageName = "js-tokens"; packageName = "js-tokens";
@ -85,6 +112,15 @@ let
sha512 = "CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw=="; sha512 = "CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==";
}; };
}; };
"tslib-2.6.2" = {
name = "tslib";
packageName = "tslib";
version = "2.6.2";
src = fetchurl {
url = "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz";
sha512 = "AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==";
};
};
}; };
args = { args = {
name = "boardwise"; name = "boardwise";
@ -92,8 +128,11 @@ let
version = "0.1.0"; version = "0.1.0";
src = ./.; src = ./.;
dependencies = [ dependencies = [
sources."@emotion/is-prop-valid-0.8.8"
sources."@emotion/memoize-0.7.4"
sources."@remix-run/router-1.13.1" sources."@remix-run/router-1.13.1"
sources."clsx-2.0.0" sources."clsx-2.0.0"
sources."framer-motion-10.16.12"
sources."js-tokens-4.0.0" sources."js-tokens-4.0.0"
sources."loose-envify-1.4.0" sources."loose-envify-1.4.0"
sources."react-18.2.0" sources."react-18.2.0"
@ -101,6 +140,7 @@ let
sources."react-router-6.20.1" sources."react-router-6.20.1"
sources."react-router-dom-6.20.1" sources."react-router-dom-6.20.1"
sources."scheduler-0.23.0" sources."scheduler-0.23.0"
sources."tslib-2.6.2"
]; ];
buildInputs = globalBuildInputs; buildInputs = globalBuildInputs;
meta = { meta = {

2253
assets/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,19 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"clsx": "^2.0.0", "clsx": "^2.0.0",
"framer-motion": "^10.16.12",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.20.1" "react-router-dom": "^6.20.1"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@types/react": "^18.2.40", "@types/react": "^18.2.40",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"typescript": "^5.3.2" "autoprefixer": "^10.4.16",
"eslint-plugin-tailwindcss": "^3.13.0",
"prettier-plugin-tailwindcss": "^0.5.7"
}, },
"private": true "private": true
} }

View File

@ -0,0 +1,8 @@
/** @type {import('prettier').Options} */
module.exports = {
arrowParens: "always",
semi: false,
tabWidth: 2,
trailingComma: "es5",
plugins: ["prettier-plugin-tailwindcss"],
}

128
assets/tailwind.config.cjs Normal file
View File

@ -0,0 +1,128 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
const plugin = require("tailwindcss/plugin")
const defaultTheme = require("tailwindcss/defaultTheme")
const fs = require("fs")
const path = require("path")
module.exports = {
content: [
"./js/**/*.{js,jsx,ts,tsx}",
"../lib/boardwise_web.ex",
"../lib/boardwise_web/**/*.*ex",
],
theme: {
fontSize: {
xs: ["0.75rem", { lineHeight: "1rem" }],
sm: ["0.875rem", { lineHeight: "1.5rem" }],
base: ["1rem", { lineHeight: "1.75rem" }],
lg: ["1.125rem", { lineHeight: "1.75rem" }],
xl: ["1.25rem", { lineHeight: "2rem" }],
"2xl": ["1.5rem", { lineHeight: "2.25rem" }],
"3xl": ["1.75rem", { lineHeight: "2.25rem" }],
"4xl": ["2rem", { lineHeight: "2.5rem" }],
"5xl": ["2.5rem", { lineHeight: "3rem" }],
"6xl": ["3rem", { lineHeight: "3.5rem" }],
"7xl": ["4rem", { lineHeight: "4.5rem" }],
},
extend: {
backgroundImage: {
"radial-gradient/black":
"radial-gradient(circle at center, black 0%, transparent 50%)",
},
borderRadius: {
"4xl": "2.5rem",
},
boxShadow: {
sm: "0 2px 4px 0 rgb(60 72 88 / 0.15)",
DEFAULT: "0 0 3px rgb(60 72 88 / 0.15)",
md: "0 5px 13px rgb(60 72 88 / 0.20)",
lg: "0 10px 25px -3px rgb(60 72 88 / 0.15)",
xl: "0 20px 25px -5px rgb(60 72 88 / 0.1), 0 8px 10px -6px rgb(60 72 88 / 0.1)",
"2xl": "0 25px 50px -12px rgb(60 72 88 / 0.25)",
inner: "inset 0 2px 4px 0 rgb(60 72 88 / 0.05)",
testi: "2px 2px 2px -1px rgb(60 72 88 / 0.15)",
},
fontFamily: {
display: [
["Mona Sans", ...defaultTheme.fontFamily.sans],
{ fontVariationSettings: '"wdth" 125' },
],
sans: ["Mona Sans", ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [
require("autoprefixer"),
require("tailwindcss"),
require("@tailwindcss/forms"),
// Allows prefixing tailwind classes with LiveView classes to add rules
// only when LiveView classes are applied, for example:
//
// <div class="phx-click-loading:animate-ping">
//
plugin(({ addVariant }) =>
addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])
),
plugin(({ addVariant }) =>
addVariant("phx-click-loading", [
".phx-click-loading&",
".phx-click-loading &",
])
),
plugin(({ addVariant }) =>
addVariant("phx-submit-loading", [
".phx-submit-loading&",
".phx-submit-loading &",
])
),
plugin(({ addVariant }) =>
addVariant("phx-change-loading", [
".phx-change-loading&",
".phx-change-loading &",
])
),
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
// See your `CoreComponents.icon/1` for more information.
//
plugin(function ({ matchComponents, theme }) {
let iconsDir = path.join(__dirname, "./vendor/heroicons/optimized")
let values = {}
let icons = [
["", "/24/outline"],
["-solid", "/24/solid"],
["-mini", "/20/solid"],
]
icons.forEach(([suffix, dir]) => {
fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => {
let name = path.basename(file, ".svg") + suffix
values[name] = { name, fullPath: path.join(iconsDir, dir, file) }
})
})
matchComponents(
{
hero: ({ name, fullPath }) => {
let content = fs
.readFileSync(fullPath)
.toString()
.replace(/\r?\n|\r/g, "")
return {
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
"-webkit-mask": `var(--hero-${name})`,
mask: `var(--hero-${name})`,
"mask-repeat": "no-repeat",
"background-color": "currentColor",
"vertical-align": "middle",
display: "inline-block",
width: theme("spacing.5"),
height: theme("spacing.5"),
}
},
},
{ values }
)
}),
],
}

View File

@ -1,68 +0,0 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
const plugin = require("tailwindcss/plugin")
const fs = require("fs")
const path = require("path")
module.exports = {
content: [
"./js/**/*.js",
"../lib/boardwise_web.ex",
"../lib/boardwise_web/**/*.*ex"
],
theme: {
extend: {
colors: {
brand: "#FD4F00",
}
},
},
plugins: [
require("@tailwindcss/forms"),
// Allows prefixing tailwind classes with LiveView classes to add rules
// only when LiveView classes are applied, for example:
//
// <div class="phx-click-loading:animate-ping">
//
plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
// See your `CoreComponents.icon/1` for more information.
//
plugin(function({matchComponents, theme}) {
let iconsDir = path.join(__dirname, "./vendor/heroicons/optimized")
let values = {}
let icons = [
["", "/24/outline"],
["-solid", "/24/solid"],
["-mini", "/20/solid"]
]
icons.forEach(([suffix, dir]) => {
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
let name = path.basename(file, ".svg") + suffix
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
})
})
matchComponents({
"hero": ({name, fullPath}) => {
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
return {
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
"-webkit-mask": `var(--hero-${name})`,
"mask": `var(--hero-${name})`,
"mask-repeat": "no-repeat",
"background-color": "currentColor",
"vertical-align": "middle",
"display": "inline-block",
"width": theme("spacing.5"),
"height": theme("spacing.5")
}
}
}, {values})
})
]
}

View File

@ -68,7 +68,7 @@ config :tailwind,
version: "3.3.5", version: "3.3.5",
default: [ default: [
args: ~w( args: ~w(
--config=tailwind.config.js --config=tailwind.config.cjs
--input=css/app.css --input=css/app.css
--output=../priv/static/assets/app.css --output=../priv/static/assets/app.css
), ),

View File

@ -25,6 +25,7 @@
tailwindcss = pkgs.nodePackages.tailwindcss.overrideAttrs (oa: { tailwindcss = pkgs.nodePackages.tailwindcss.overrideAttrs (oa: {
plugins = [ plugins = [
pkgs.nodePackages.autoprefixer
pkgs.nodePackages."@tailwindcss/forms" pkgs.nodePackages."@tailwindcss/forms"
]; ];
}); });

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en" class="h-full antialiased">
<head>
<meta charset="utf-8" />
<meta
name="description"
content="BoardWise - Level up your chess game with a world-class coach."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<link rel="icon" href={~p"/favicon.ico"} type="image/x-icon" />
<title>BoardWise</title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer type="text/javascript" src={~p"/assets/react/main.js"}>
</script>
</head>
<body class="flex min-h-full flex-col text-base text-black">
<%= @inner_content %>
</body>
</html>

View File

@ -15,8 +15,6 @@
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} /> <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}> <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script> </script>
<script defer type="text/javascript" src={~p"/assets/react/main.js"}>
</script>
</head> </head>
<body class="bg-white antialiased"> <body class="bg-white antialiased">
<%= @inner_content %> <%= @inner_content %>

View File

@ -1,11 +1,11 @@
defmodule BoardWiseWeb.ReactController do defmodule BoardWiseWeb.ReactController do
use BoardWiseWeb, :controller use BoardWiseWeb, :controller
def index(conn, _params) do def mount(conn, _params) do
# Set `layout` to false to bypass the app layout. The goal here is to # Set `layout` to false to bypass the app layout. The goal here is to
# eventually migrate away from the React app as is defined in favor of # eventually migrate away from the React app as is defined in favor of
# Phoenix related components. Exposing this mount point is the first step # Phoenix related components. Exposing this mount point is the first step
# in migrating away from Vercel into a self-hosted solution. # in migrating away from Vercel into a self-hosted solution.
render(conn, :index, layout: false) render(conn, :mount, layout: false)
end end
end end

View File

@ -1 +0,0 @@
<div id="mount"></div>

View File

@ -0,0 +1 @@
<div id="mount" class="flex min-h-full flex-col text-base text-black"></div>

View File

@ -10,14 +10,16 @@ defmodule BoardWiseWeb.Router do
plug :put_secure_browser_headers plug :put_secure_browser_headers
end end
pipeline :react do
plug :put_root_layout, html: {BoardWiseWeb.Layouts, :react}
end
pipeline :api do pipeline :api do
plug :accepts, ["json"] plug :accepts, ["json"]
end end
scope "/", BoardWiseWeb do scope "/", BoardWiseWeb do
pipe_through :browser pipe_through :browser
get "/*path", ReactController, :index
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.
@ -41,4 +43,11 @@ defmodule BoardWiseWeb.Router do
forward "/mailbox", Plug.Swoosh.MailboxPreview forward "/mailbox", Plug.Swoosh.MailboxPreview
end end
end end
# A catch-all that defers to the React app router.
scope "/", BoardWiseWeb do
pipe_through [:browser, :react]
get "/*path", ReactController, :mount
end
end end