Compare commits

...

4 Commits

Author SHA1 Message Date
03048b30b1 Add deployment to documentation 2025-11-02 20:43:07 -05:00
820a83feef Add docker support 2025-11-02 20:08:31 -05:00
fbb210f083 Add desktop support 2025-11-02 19:57:59 -05:00
7abf14e9ae Add more sections 2025-11-02 19:43:39 -05:00
12 changed files with 274 additions and 29 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
LOCAL_PORT=3000

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env

12
Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM node:alpine AS build
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD [ "nginx", "-g", "daemon off;" ]

View File

@ -1,6 +1,8 @@
# BCDigital Code Challenge
My submission to the code challenge from [BCDigial](https://www.bc-digital.ca/)
See the website at https://bcdigital-challenge.thingsnstuff.xyz/
My submission to the code challenge from [BC Digital](https://www.bc-digital.ca/) for the Frontend Developer position
Landing page for hypthetical client **Great Music LLM**'s event and employee management software
@ -8,7 +10,8 @@ Landing page for hypthetical client **Great Music LLM**'s event and employee man
- [`vite`](https://vite.dev/) build tooling
- [`tailwindcss`](https://tailwindcss.com/) styling
- [Magic UI](https://magicui.design/) component library
- [Magic UI](https://magicui.design/) component library (see [`src/components/ui`](https://git.thingsnstuff.xyz/ethanglide/bcdigital-challenge/src/branch/main/src/components/ui))
- [`prettier`](https://prettier.io/) code formatting
- [`eslint`](https://eslint.org/) code linting
- [`husky`](https://typicode.github.io/husky/) and [`lint-staged`](https://github.com/lint-staged/lint-staged) pre-commit hooks for testing/formatting/linting
- [Docker](https://www.docker.com/) containerization

6
docker-compose.yml Normal file
View File

@ -0,0 +1,6 @@
services:
nginx:
build: .
ports:
- "${LOCAL_PORT}:80"
restart: always

7
nginx.conf Normal file
View File

@ -0,0 +1,7 @@
server {
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}

View File

@ -1,7 +1,9 @@
import { StripedPattern } from "./components/magicui/striped-pattern";
import { Navbar } from "./components/navbar/navbar";
import { PartnersMarquee } from "./components/partners-marquee/partners-marquee";
import { AuroraText } from "./components/ui/aurora-text";
import { DotPattern } from "./components/ui/dot-pattern";
import { GridPattern } from "./components/ui/grid-pattern";
import { RainbowButton } from "./components/ui/rainbow-button";
import { TextAnimate } from "./components/ui/text-animate";
@ -18,14 +20,14 @@ export function App() {
<div className="h-full text-white flex items-center justify-center backdrop-blur-[3px] backdrop-grayscale-50">
<div className="flex flex-col gap-16 items-center text-center">
<div className="flex flex-col gap-4 items-center">
<h2 className="text-4xl font-bold">
<h2 className="text-4xl font-bold sm:text-6xl">
<AuroraText>Orchestral</AuroraText> Event Management
</h2>
<TextAnimate
animation="blurInUp"
by="character"
as="h3"
className="text-xl"
className="text-xl sm:text-3xl"
>
Without The Hassle
</TextAnimate>
@ -34,37 +36,127 @@ export function App() {
</div>
</div>
</div>
<div className="relative overflow-hidden py-5 flex flex-col gap-5">
<div className="relative overflow-hidden py-5 flex flex-col gap-5 sm:py-10">
<TextAnimate
animation="blurInUp"
by="character"
as="h3"
className="text-xl font-bold text-center"
className="text-xl font-bold text-center sm:text-3xl"
>
Our Partners
</TextAnimate>
<PartnersMarquee />
<span></span> {/* Spacer for DotPattern */}
<DotPattern className="-z-10 mask-[radial-gradient(300px_circle_at_center,white,transparent)]" />
<DotPattern className="-z-10" />
</div>
<div className="flex flex-col gap-5 p-6 bg-stone-200 text-center">
<div className="bg-[url(https://cdn.toontrack.com/app/uploads/product/upright-ebx/jpg/upright-ebx-top-mobile.jpg)] bg-cover bg-center text-white">
<div className="flex flex-col gap-5 p-6 text-center sm:px-[100px] md:px-[200px] lg:px-[300px] xl:px-[400px] backdrop-blur-[5px] backdrop-grayscale-25 sm:py-10">
<TextAnimate
animation="blurInUp"
by="character"
as="h3"
className="text-xl font-bold text-center"
className="text-xl font-bold text-center sm:text-3xl"
>
About Us
</TextAnimate>
<p>
We are dedicated to simplifying orchestral event management through
innovative solutions tailored to the unique needs of orchestras and
their audiences.
<p className="sm:text-lg">
We are dedicated to simplifying orchestral event management
through innovative solutions tailored to the unique needs of
orchestras and their audiences.
</p>
<p>
<p className="sm:text-lg">
Our platform streamlines the planning, coordination, and execution
of orchestral events, allowing musicians and organizers to focus on
what they do best: creating unforgettable musical experiences.
of orchestral events, allowing musicians and organizers to focus
on what they do best: creating unforgettable musical experiences.
</p>
</div>
</div>
<div className="relative flex flex-col gap-5 p-6 text-center sm:px-[100px] md:px-[200px] lg:px-[300px] xl:px-[400px] backdrop-blur-[6px] sm:py-10">
<TextAnimate
animation="blurInUp"
by="character"
as="h3"
className="text-xl font-bold text-center sm:text-3xl"
>
Events
</TextAnimate>
<p className="sm:text-lg">
Are you an event organizer looking to host an orchestral
performance? Our platform connects you with talented orchestras and
provides the tools you need to plan and execute a successful event.
</p>
<div className="flex justify-center">
<RainbowButton variant="outline" className="w-fit">
Events Services
</RainbowButton>
</div>
<GridPattern strokeDasharray="4 2" className="-z-10" />
</div>
<div className="bg-[url(https://images.stockcake.com/public/1/3/a/13a0d24a-a714-43b5-b462-636017c01d60_large/grand-piano-interior-stockcake.jpg)] bg-cover bg-center text-white">
<div className="flex flex-col gap-5 p-6 text-center sm:px-[100px] md:px-[200px] lg:px-[300px] xl:px-[400px] backdrop-blur-[3px] backdrop-grayscale-50 sm:py-10">
<TextAnimate
animation="blurInUp"
by="character"
as="h3"
className="text-xl font-bold text-center sm:text-3xl"
>
Employee Management
</TextAnimate>
<p className="sm:text-lg">
Are you an orchestra looking to manage your musicians and staff
more effectively? Our platform offers comprehensive employee
management solutions designed specifically for orchestras.
</p>
<div className="flex justify-center">
<RainbowButton variant="outline" className="w-fit">
Employee Management Services
</RainbowButton>
</div>
</div>
</div>
<div className="relative flex flex-col gap-4 p-6 sm:px-[100px] md:px-[200px] lg:px-[300px] xl:px-[400px] sm:py-10 sm:flex-row sm:justify-around">
<h3 className="font-alex-brush text-2xl sm:text-3xl">
Great Music LLM
</h3>
<div className="flex flex-col gap-2">
<h4 className="font-bold text-lg sm:text-xl">Services</h4>
<div className="flex flex-col gap-1">
<a
href="#"
className="text-gray-500 hover:text-gray-600 hover:underline sm:text-lg"
>
Events
</a>
<a
href="#"
className="text-gray-500 hover:text-gray-600 hover:underline sm:text-lg"
>
Employee Management
</a>
</div>
</div>
<div className="flex flex-col gap-2">
<h4 className="font-bold text-lg sm:text-xl">Company</h4>
<div className="flex flex-col gap-1">
<a
href="#"
className="text-gray-500 hover:text-gray-600 hover:underline sm:text-lg"
>
Terms &amp; Conditions
</a>
<a
href="#"
className="text-gray-500 hover:text-gray-600 hover:underline sm:text-lg"
>
Privacy Policy
</a>
</div>
</div>
<StripedPattern className="-z-10 text-gray-300" />
</div>
<div className="p-2 border-t">
<p className="text-center text-xs">
© 2025 Great Music LLM | All rights reserved.
</p>
</div>
</main>

View File

@ -0,0 +1,50 @@
import React, { useId } from "react";
import { cn } from "@/lib/utils";
interface StripedPatternProps extends React.SVGProps<SVGSVGElement> {
direction?: "left" | "right";
}
export function StripedPattern({
direction = "left",
className,
width = 10,
height = 10,
...props
}: StripedPatternProps) {
const id = useId();
const w = Number(width);
const h = Number(height);
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 z-10 h-full w-full stroke-[0.5]",
className,
)}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<defs>
<pattern id={id} width={w} height={h} patternUnits="userSpaceOnUse">
{direction === "left" ? (
<>
<line x1="0" y1={h} x2={w} y2="0" stroke="currentColor" />
<line x1={-w} y1={h} x2="0" y2="0" stroke="currentColor" />
<line x1={w} y1={h} x2={w * 2} y2="0" stroke="currentColor" />
</>
) : (
<>
<line x1="0" y1="0" x2={w} y2={h} stroke="currentColor" />
<line x1={-w} y1="0" x2="0" y2={h} stroke="currentColor" />
<line x1={w} y1="0" x2={w * 2} y2={h} stroke="currentColor" />
</>
)}
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${id})`} />
</svg>
);
}

View File

@ -1,14 +1,16 @@
import { RainbowButton } from "../ui/rainbow-button";
export function Navbar() {
return (
<nav className="flex justify-between items-center p-4 px-6 shadow-md">
<h1 className="text-2xl font-bold font-alex-brush">Great Music LLM</h1>
<div className="hidden sm:flex gap-2">
<button className="bg-blue-500 text-white px-4 py-2 rounded">
Button 1
</button>
<button className="bg-green-500 text-white px-4 py-2 rounded">
Button 2
</button>
<RainbowButton variant="outline" className="w-fit">
Events Services
</RainbowButton>
<RainbowButton variant="outline" className="w-fit">
Employee Management Services
</RainbowButton>
</div>
</nav>
);

View File

@ -122,7 +122,7 @@ export function DotPattern({
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</radialGradient>
</defs>
{dots.map((dot, index) => (
{dots.map((dot) => (
<motion.circle
key={`${dot.x}-${dot.y}`}
cx={dot.x}

View File

@ -0,0 +1,70 @@
import { useId } from "react";
import { cn } from "@/lib/utils";
interface GridPatternProps extends React.SVGProps<SVGSVGElement> {
width?: number;
height?: number;
x?: number;
y?: number;
squares?: Array<[x: number, y: number]>;
strokeDasharray?: string;
className?: string;
[key: string]: unknown;
}
export function GridPattern({
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = "0",
squares,
className,
...props
}: GridPatternProps) {
const id = useId();
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
{squares && (
<svg x={x} y={y} className="overflow-visible">
{squares.map(([x, y]) => (
<rect
strokeWidth="0"
key={`${x}-${y}`}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
/>
))}
</svg>
)}
</svg>
);
}

View File

@ -46,7 +46,7 @@ export function Marquee({
<div
{...props}
className={cn(
"group flex [gap:var(--gap)] overflow-hidden p-2 [--duration:40s] [--gap:1rem]",
"group flex gap-(--gap) overflow-hidden p-2 [--duration:40s] [--gap:1rem]",
{
"flex-row": !vertical,
"flex-col": vertical,
@ -59,11 +59,11 @@ export function Marquee({
.map((_, i) => (
<div
key={i}
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
className={cn("flex shrink-0 justify-around gap-(--gap)", {
"animate-marquee flex-row": !vertical,
"animate-marquee-vertical flex-col": vertical,
"group-hover:[animation-play-state:paused]": pauseOnHover,
"[animation-direction:reverse]": reverse,
"group-hover:paused": pauseOnHover,
"direction-[reverse]": reverse,
})}
>
{children}