Migrate the root layout from Vercel. (#2)
parent
ccc8423e0f
commit
a1dd46561d
|
@ -1,24 +1,32 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
mixFiles=$(
|
||||
git --no-pager diff --name-status --no-color --cached | \
|
||||
awk '$1 != "D" && $2 ~ /\.exs?$/ {print $NF}'
|
||||
STAGED=$(
|
||||
git --no-pager diff --name-only --no-color --cached --diff-filter=d |
|
||||
# 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
|
||||
mix format "$path"
|
||||
git add "$path"
|
||||
done
|
||||
if [[ "$FILENAME" =~ .*\.exs? ]]; then
|
||||
MIX_TARGETS+=("${FILENAME}")
|
||||
elif
|
||||
[[ "$FILENAME" =~ assets/.*\.jsx? ]] ||
|
||||
[[ "$FILENAME" =~ assets/.*\.tsx? ]]; then
|
||||
WEB_TARGETS+=("${FILENAME#"assets/"}")
|
||||
fi
|
||||
done <<< "$STAGED"
|
||||
|
||||
webFiles=$(
|
||||
git --no-pager diff --name-status --no-color --cached | \
|
||||
awk '$1 != "D" && $2 ~ /\.jsx?$|\.tsx?$/ {print $NF}'
|
||||
)
|
||||
if (( ${#MIX_TARGETS[@]} )); then
|
||||
mix format "${MIX_TARGETS[@]}"
|
||||
git add "${MIX_TARGETS[@]}"
|
||||
fi
|
||||
|
||||
for path in $webFiles
|
||||
do
|
||||
prettier --write "$path"
|
||||
git add "$path"
|
||||
done
|
||||
if (( ${#WEB_TARGETS[@]} )); then
|
||||
cd assets
|
||||
npx prettier --write "${WEB_TARGETS[@]}"
|
||||
git add "${WEB_TARGETS[@]}"
|
||||
fi
|
||||
|
|
|
@ -14,16 +14,19 @@ html {
|
|||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
touch-action: manipulation;
|
||||
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0;
|
||||
font-feature-settings:
|
||||
"case" 1,
|
||||
"rlig" 1,
|
||||
"calt" 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue',
|
||||
'Helvetica', sans-serif;
|
||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Helvetica Neue",
|
||||
"Helvetica", sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@apply text-white bg-white antialiased;
|
||||
@apply bg-white text-white antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
@ -22,13 +22,17 @@ import { Socket } from "phoenix"
|
|||
import { LiveSocket } from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })
|
||||
let csrfToken = document
|
||||
.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
|
||||
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-stop", _info => topbar.hide())
|
||||
window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300))
|
||||
window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide())
|
||||
|
||||
// connect if there are any LiveViews on the page
|
||||
liveSocket.connect()
|
||||
|
@ -38,4 +42,3 @@ liveSocket.connect()
|
|||
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
|
||||
// >> liveSocket.disableLatencySim()
|
||||
window.liveSocket = liveSocket
|
||||
|
||||
|
|
|
@ -1,25 +1,13 @@
|
|||
import * as React from "react";
|
||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
import { Footer } from "./components/Footer";
|
||||
import * as React from "react"
|
||||
import { RouterProvider } from "react-router-dom"
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Footer />, // Placeholder.
|
||||
},
|
||||
{
|
||||
path: "/nested",
|
||||
element: <Footer />, // Placeholder.
|
||||
},
|
||||
]);
|
||||
import { RootLayout } from "./components/RootLayout"
|
||||
import { router } from "./router"
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div>
|
||||
<main className="w-full flex-auto">
|
||||
<RouterProvider router={router} />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
<RootLayout>
|
||||
<RouterProvider router={router} />
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as React from "react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react"
|
||||
import clsx from "clsx"
|
||||
|
||||
type ContainerProps<T extends React.ElementType> = {
|
||||
as?: T;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
as?: T
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Container<T extends React.ElementType = "div">({
|
||||
as,
|
||||
|
@ -13,11 +13,11 @@ export function Container<T extends React.ElementType = "div">({
|
|||
children,
|
||||
}: Omit<React.ComponentPropsWithoutRef<T>, keyof ContainerProps<T>> &
|
||||
ContainerProps<T>) {
|
||||
let Component = as ?? "div";
|
||||
let Component = as ?? "div"
|
||||
|
||||
return (
|
||||
<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>
|
||||
</Component>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as React from "react"
|
||||
|
||||
import { Container } from "./Container";
|
||||
import { Logo } from "./Logo";
|
||||
import { Container } from "./Container"
|
||||
import { Logo } from "./Logo"
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
|
@ -15,7 +15,7 @@ const navigation = [
|
|||
{ title: "Contact Us", href: "/contact/" },
|
||||
],
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
function Navigation() {
|
||||
return (
|
||||
|
@ -42,7 +42,7 @@ function Navigation() {
|
|||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function Footer() {
|
||||
|
@ -60,5 +60,5 @@ export function Footer() {
|
|||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react"
|
||||
import clsx from "clsx"
|
||||
|
||||
import LogoMark from "../icons/Logomark";
|
||||
import LogoMark from "../icons/Logomark"
|
||||
|
||||
export function Logo({
|
||||
invert = false,
|
||||
|
@ -15,11 +15,11 @@ export function Logo({
|
|||
<p
|
||||
className={clsx(
|
||||
"font-display text-xl font-bold tracking-tight",
|
||||
invert ? "text-white" : "text-neutral-950",
|
||||
invert ? "text-white" : "text-neutral-950"
|
||||
)}
|
||||
>
|
||||
BoardWise
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,22 +1,22 @@
|
|||
import * as React from "react";
|
||||
import * as React from "react"
|
||||
|
||||
const SvgComponent = ({ invert = false, size = 25, ...props }) => {
|
||||
const color = invert ? "rgb(255, 255, 255)" : "rgb(10 10 10)";
|
||||
const radius = 5;
|
||||
const color = invert ? "rgb(255, 255, 255)" : "rgb(10 10 10)"
|
||||
const radius = 5
|
||||
const roundedTopRightPath = `
|
||||
M ${size / 2} 0
|
||||
H ${size - radius}
|
||||
Q ${size} 0, ${size} ${radius}
|
||||
V ${size / 2}
|
||||
H ${size / 2}
|
||||
Z`;
|
||||
Z`
|
||||
const roundedBottomLeftPath = `
|
||||
M 0 ${size - radius}
|
||||
Q 0 ${size}, ${radius} ${size}
|
||||
H ${size / 2}
|
||||
V ${size / 2}
|
||||
H 0
|
||||
Z`;
|
||||
Z`
|
||||
|
||||
return (
|
||||
<svg
|
||||
|
@ -28,6 +28,7 @@ const SvgComponent = ({ invert = false, size = 25, ...props }) => {
|
|||
<path d={roundedTopRightPath} fill={color} />
|
||||
<path d={roundedBottomLeftPath} fill={color} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export default SvgComponent;
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgComponent
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import * as React from "react"
|
||||
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import ReactDOM from "react-dom/client"
|
||||
import App from "./App"
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('mount')!).render(<App />)
|
||||
ReactDOM.createRoot(document.getElementById("mount")!).render(<App />)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import * as React from "react"
|
||||
import { createBrowserRouter } from "react-router-dom"
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <div />,
|
||||
},
|
||||
])
|
|
@ -4,6 +4,24 @@
|
|||
|
||||
let
|
||||
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" = {
|
||||
name = "_at_remix-run_slash_router";
|
||||
packageName = "@remix-run/router";
|
||||
|
@ -22,6 +40,15 @@ let
|
|||
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" = {
|
||||
name = "js-tokens";
|
||||
packageName = "js-tokens";
|
||||
|
@ -85,6 +112,15 @@ let
|
|||
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 = {
|
||||
name = "boardwise";
|
||||
|
@ -92,8 +128,11 @@ let
|
|||
version = "0.1.0";
|
||||
src = ./.;
|
||||
dependencies = [
|
||||
sources."@emotion/is-prop-valid-0.8.8"
|
||||
sources."@emotion/memoize-0.7.4"
|
||||
sources."@remix-run/router-1.13.1"
|
||||
sources."clsx-2.0.0"
|
||||
sources."framer-motion-10.16.12"
|
||||
sources."js-tokens-4.0.0"
|
||||
sources."loose-envify-1.4.0"
|
||||
sources."react-18.2.0"
|
||||
|
@ -101,6 +140,7 @@ let
|
|||
sources."react-router-6.20.1"
|
||||
sources."react-router-dom-6.20.1"
|
||||
sources."scheduler-0.23.0"
|
||||
sources."tslib-2.6.2"
|
||||
];
|
||||
buildInputs = globalBuildInputs;
|
||||
meta = {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,15 +3,19 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"framer-motion": "^10.16.12",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@types/react": "^18.2.40",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('prettier').Options} */
|
||||
module.exports = {
|
||||
arrowParens: "always",
|
||||
semi: false,
|
||||
tabWidth: 2,
|
||||
trailingComma: "es5",
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
}
|
|
@ -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 }
|
||||
)
|
||||
}),
|
||||
],
|
||||
}
|
|
@ -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})
|
||||
})
|
||||
]
|
||||
}
|
|
@ -68,7 +68,7 @@ config :tailwind,
|
|||
version: "3.3.5",
|
||||
default: [
|
||||
args: ~w(
|
||||
--config=tailwind.config.js
|
||||
--config=tailwind.config.cjs
|
||||
--input=css/app.css
|
||||
--output=../priv/static/assets/app.css
|
||||
),
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
tailwindcss = pkgs.nodePackages.tailwindcss.overrideAttrs (oa: {
|
||||
plugins = [
|
||||
pkgs.nodePackages.autoprefixer
|
||||
pkgs.nodePackages."@tailwindcss/forms"
|
||||
];
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -15,8 +15,6 @@
|
|||
<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>
|
||||
<script defer type="text/javascript" src={~p"/assets/react/main.js"}>
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white antialiased">
|
||||
<%= @inner_content %>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
defmodule BoardWiseWeb.ReactController do
|
||||
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
|
||||
# eventually migrate away from the React app as is defined in favor of
|
||||
# Phoenix related components. Exposing this mount point is the first step
|
||||
# in migrating away from Vercel into a self-hosted solution.
|
||||
render(conn, :index, layout: false)
|
||||
render(conn, :mount, layout: false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<div id="mount"></div>
|
|
@ -0,0 +1 @@
|
|||
<div id="mount" class="flex min-h-full flex-col text-base text-black"></div>
|
|
@ -10,14 +10,16 @@ defmodule BoardWiseWeb.Router do
|
|||
plug :put_secure_browser_headers
|
||||
end
|
||||
|
||||
pipeline :react do
|
||||
plug :put_root_layout, html: {BoardWiseWeb.Layouts, :react}
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["json"]
|
||||
end
|
||||
|
||||
scope "/", BoardWiseWeb do
|
||||
pipe_through :browser
|
||||
|
||||
get "/*path", ReactController, :index
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
|
@ -41,4 +43,11 @@ defmodule BoardWiseWeb.Router do
|
|||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue