move ot mantine frontend

This commit is contained in:
Craig
2025-03-26 14:08:03 +00:00
parent f9c6b607cd
commit 55f56123ad
83 changed files with 11349 additions and 6007 deletions

View File

@@ -0,0 +1,27 @@
name: npm test
on:
pull_request:
branches:
- '**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
test_pull_request:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: '**/yarn.lock'
- name: Install dependencies
run: yarn
- name: Run build
run: npm run build
- name: Run tests
run: npm test

132
frontend/vite-template-master/.gitignore vendored Normal file
View File

@@ -0,0 +1,132 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store

View File

@@ -0,0 +1 @@
v22.11.0

View File

@@ -0,0 +1,35 @@
/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */
const config = {
printWidth: 100,
singleQuote: true,
trailingComma: 'es5',
plugins: ['@ianvs/prettier-plugin-sort-imports'],
importOrder: [
'.*styles.css$',
'',
'dayjs',
'^react$',
'^next$',
'^next/.*$',
'<BUILTIN_MODULES>',
'<THIRD_PARTY_MODULES>',
'^@mantine/(.*)$',
'^@mantinex/(.*)$',
'^@mantine-tests/(.*)$',
'^@docs/(.*)$',
'^@/.*$',
'^../(?!.*.css$).*$',
'^./(?!.*.css$).*$',
'\\.css$',
],
overrides: [
{
files: '*.mdx',
options: {
printWidth: 70,
},
},
],
};
export default config;

View File

@@ -0,0 +1,17 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
core: {
disableWhatsNewNotifications: true,
disableTelemetry: true,
enableCrashReports: false,
},
stories: ['../src/**/*.mdx', '../src/**/*.story.@(js|jsx|ts|tsx)'],
addons: ['storybook-dark-mode'],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
export default config;

View File

@@ -0,0 +1,33 @@
import '@mantine/core/styles.css';
import React, { useEffect } from 'react';
import { addons } from '@storybook/preview-api';
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
import { MantineProvider, useMantineColorScheme } from '@mantine/core';
import { theme } from '../src/theme';
const channel = addons.getChannel();
export const parameters = {
layout: 'fullscreen',
options: {
showPanel: false,
},
};
function ColorSchemeWrapper({ children }: { children: React.ReactNode }) {
const { setColorScheme } = useMantineColorScheme();
const handleColorScheme = (value: boolean) => setColorScheme(value ? 'dark' : 'light');
useEffect(() => {
channel.on(DARK_MODE_EVENT_NAME, handleColorScheme);
return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme);
}, [channel]);
return children;
}
export const decorators = [
(renderStory: any) => <ColorSchemeWrapper>{renderStory()}</ColorSchemeWrapper>,
(renderStory: any) => <MantineProvider theme={theme}>{renderStory()}</MantineProvider>,
];

View File

@@ -0,0 +1 @@
dist

View File

@@ -0,0 +1,28 @@
{
"extends": ["stylelint-config-standard-scss"],
"rules": {
"custom-property-pattern": null,
"selector-class-pattern": null,
"scss/no-duplicate-mixins": null,
"declaration-empty-line-before": null,
"declaration-block-no-redundant-longhand-properties": null,
"alpha-value-notation": null,
"custom-property-empty-line-before": null,
"property-no-vendor-prefix": null,
"color-function-notation": null,
"length-zero-no-unit": null,
"selector-not-notation": null,
"no-descending-specificity": null,
"comment-empty-line-before": null,
"scss/at-mixin-pattern": null,
"scss/at-rule-no-unknown": null,
"value-keyword-case": null,
"media-feature-range-notation": null,
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global"]
}
]
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.7.0.cjs

View File

@@ -0,0 +1,34 @@
# Mantine Vite template
## Features
This template comes with the following features:
- [PostCSS](https://postcss.org/) with [mantine-postcss-preset](https://mantine.dev/styles/postcss-preset)
- [TypeScript](https://www.typescriptlang.org/)
- [Storybook](https://storybook.js.org/)
- [Vitest](https://vitest.dev/) setup with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
- ESLint setup with [eslint-config-mantine](https://github.com/mantinedev/eslint-config-mantine)
## npm scripts
## Build and dev scripts
- `dev` start development server
- `build` build production version of the app
- `preview` locally preview production build
### Testing scripts
- `typecheck` checks TypeScript types
- `lint` runs ESLint
- `prettier:check` checks files with Prettier
- `vitest` runs vitest tests
- `vitest:watch` starts vitest watch
- `test` runs `vitest`, `prettier:check`, `lint` and `typecheck` scripts
### Other scripts
- `storybook` starts storybook dev server
- `storybook:build` build production storybook bundle to `storybook-static`
- `prettier:write` formats all files with Prettier

View File

@@ -0,0 +1,11 @@
import mantine from 'eslint-config-mantine';
import tseslint from 'typescript-eslint';
export default tseslint.config(
...mantine,
{ ignores: ['**/*.{mjs,cjs,js,d.ts,d.mts}', './.storybook/main.ts'] },
{
files: ['**/*.story.tsx'],
rules: { 'no-console': 'off' },
}
);

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
/>
<title>Vite + Mantine App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
{
"name": "mantine-vite-template",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"lint": "npm run eslint && npm run stylelint",
"eslint": "eslint . --cache",
"stylelint": "stylelint '**/*.css' --cache",
"prettier": "prettier --check \"**/*.{ts,tsx}\"",
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
"vitest": "vitest run",
"vitest:watch": "vitest",
"test": "npm run typecheck && npm run prettier && npm run lint && npm run vitest && npm run build",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build"
},
"dependencies": {
"@mantine/carousel": "^7.17.2",
"@mantine/core": "7.17.2",
"@mantine/hooks": "7.17.2",
"@mantinex/mantine-logo": "^1.1.0",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.4.0"
},
"devDependencies": {
"@eslint/js": "^9.23.0",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@storybook/react": "^8.6.8",
"@storybook/react-vite": "^8.6.8",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.13.11",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.23.0",
"eslint-config-mantine": "^4.0.3",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.4",
"identity-obj-proxy": "^3.0.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.3",
"postcss-preset-mantine": "1.17.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.5.3",
"prop-types": "^15.8.1",
"storybook": "^8.6.8",
"storybook-dark-mode": "^4.0.2",
"stylelint": "^16.16.0",
"stylelint-config-standard-scss": "^14.0.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.27.0",
"vite": "^6.2.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9"
},
"packageManager": "yarn@4.7.0"
}

View File

@@ -0,0 +1,14 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};

View File

@@ -0,0 +1,13 @@
import '@mantine/core/styles.css';
import { MantineProvider } from '@mantine/core';
import { Router } from './Router';
import { theme } from './theme';
export default function App() {
return (
<MantineProvider theme={theme}>
<Router />
</MantineProvider>
);
}

View File

@@ -0,0 +1,13 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { HomePage } from './pages/Home.page';
const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
},
]);
export function Router() {
return <RouterProvider router={router} />;
}

View File

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 372 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,24 @@
.icon {
width: 22px;
height: 22px;
}
.dark {
@mixin dark {
display: none;
}
@mixin light {
display: block;
}
}
.light {
@mixin light {
display: none;
}
@mixin dark {
display: block;
}
}

View File

@@ -0,0 +1,21 @@
import { ActionIcon, useMantineColorScheme } from '@mantine/core';
import { TbSun, TbMoon } from 'react-icons/tb';
import cx from 'clsx';
import classes from './ColorSchemeToggle.module.css';
export function ColorSchemeToggle() {
// from https://mantine.dev/theming/color-schemes/
const { colorScheme, setColorScheme } = useMantineColorScheme();
return (
<ActionIcon
onClick={() => setColorScheme(colorScheme === 'light' ? 'dark' : 'light')}
variant="default"
size="xl"
aria-label="Toggle color scheme"
>
<TbSun className={cx(classes.icon, classes.light)} />
<TbMoon className={cx(classes.icon, classes.dark)} />
</ActionIcon>
);
}

View File

@@ -0,0 +1,12 @@
.wrapper {
padding: var(--mantine-spacing-xl);
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
border-radius: var(--mantine-radius-md);
box-shadow: var(--mantine-shadow-lg);
margin-top: 20px;
}
.title {
line-height: 1.2;
}

View File

@@ -0,0 +1,21 @@
import { Container, Stack, Title, Text, Anchor } from '@mantine/core';
import classes from './Contact.module.css';
export function Contact() {
return (
<Container className={classes.wrapper}>
<Stack gap="md">
<Title className={classes.title}>Get in Touch</Title>
<Text className={classes.description}>
If you'd like to collaborate, have any questions, or just want to say hello, feel free to reach out!
</Text>
<Text>
Email: <Anchor href="mailto:cdmacfadyen@proton.me">cdmacfadyen@proton.me</Anchor>
</Text>
<Text>
LinkedIn: <Anchor href="https://www.linkedin.com/in/craig-macfadyen-9a2041197" target="_blank">linkedin.com/in/craigmacfadyen</Anchor>
</Text>
</Stack>
</Container>
);
}

View File

@@ -0,0 +1,18 @@
.wrapper {
display: flex;
align-items: center;
color: var(--mantine-color-white);
}
.icon {
margin-right: var(--mantine-spacing-md);
background-color: transparent;
}
.title {
color: var(--mantine-color-blue-0);
}
.description {
color: var(--mantine-color-white);
}

View File

@@ -0,0 +1,35 @@
import { TbAt, TbMapPin, TbPhone, TbSun } from 'react-icons/tb';
import { Box, Stack, Text } from '@mantine/core';
import classes from './ContactIcons.module.css';
interface ContactIconProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'title'> {
icon: typeof TbSun;
title: React.ReactNode;
description: React.ReactNode;
}
function ContactIcon({ icon: Icon, title, description, ...others }: ContactIconProps) {
return (
<div className={classes.wrapper} {...others}>
<Box mr="md">
<Icon size={24} />
</Box>
<div>
<Text size="xs" className={classes.title}>
{title}
</Text>
<Text className={classes.description}>{description}</Text>
</div>
</div>
);
}
const MOCKDATA = [
{ title: 'Email', description: 'hello@mantine.dev', icon: TbAt },
];
export function ContactIconsList() {
const items = MOCKDATA.map((item, index) => <ContactIcon key={index} {...item} />);
return <Stack>{items}</Stack>;
}

View File

@@ -0,0 +1,64 @@
.wrapper {
min-height: 400px;
background-image: linear-gradient(
-60deg,
var(--mantine-color-blue-4) 0%,
var(--mantine-color-blue-7) 100%
);
border-radius: var(--mantine-radius-md);
padding: calc(var(--mantine-spacing-xl) * 2.5);
@media (max-width: $mantine-breakpoint-sm) {
padding: calc(var(--mantine-spacing-xl) * 1.5);
}
}
.title {
font-family:
Greycliff CF,
var(--mantine-font-family);
color: var(--mantine-color-white);
line-height: 1;
}
.description {
color: var(--mantine-color-blue-0);
max-width: 300px;
@media (max-width: $mantine-breakpoint-sm) {
max-width: 100%;
}
}
.form {
background-color: var(--mantine-color-white);
padding: var(--mantine-spacing-xl);
border-radius: var(--mantine-radius-md);
box-shadow: var(--mantine-shadow-lg);
}
.social {
color: var(--mantine-color-white);
@mixin hover {
color: var(--mantine-color-blue-1);
}
}
.input {
background-color: var(--mantine-color-white);
border-color: var(--mantine-color-gray-4);
color: var(--mantine-color-black);
&::placeholder {
color: var(--mantine-color-gray-5);
}
}
.inputLabel {
color: var(--mantine-color-black);
}
.control {
background-color: var(--mantine-color-blue-6);
}

View File

@@ -0,0 +1,66 @@
import { TbBrandInstagram, TbBrandTwitter, TbBrandYoutube } from 'react-icons/tb';
import {
ActionIcon,
Button,
Group,
SimpleGrid,
Text,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { ContactIconsList } from './ContactIcons';
import classes from './ContactUs.module.css';
const social = [TbBrandInstagram, TbBrandTwitter, TbBrandYoutube];
export function ContactUs() {
const icons = social.map((Icon, index) => (
<ActionIcon key={index} size={28} className={classes.social} variant="transparent">
<Icon size={22} stroke={1.5} />
</ActionIcon>
));
return (
<div className={classes.wrapper}>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing={50}>
<div>
<Title className={classes.title}>Contact us</Title>
<Text className={classes.description} mt="sm" mb={30}>
Get in touch
</Text>
<ContactIconsList />
<Group mt="xl">{icons}</Group>
</div>
<div className={classes.form}>
<TextInput
label="Email"
placeholder="your@email.com"
required
classNames={{ input: classes.input, label: classes.inputLabel }}
/>
<TextInput
label="Name"
placeholder="John Doe"
mt="md"
classNames={{ input: classes.input, label: classes.inputLabel }}
/>
<Textarea
required
label="Your message"
placeholder="I want to order your goods"
minRows={4}
mt="md"
classNames={{ input: classes.input, label: classes.inputLabel }}
/>
<Group justify="flex-end" mt="md">
<Button className={classes.control}>Send message</Button>
</Group>
</div>
</SimpleGrid>
</div>
);
}

View File

@@ -0,0 +1,3 @@
.section {
margin-top: 80px;
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Button, Text, Title, Container, Stack, Grid, Image, Center } from '@mantine/core';
import { Carousel } from '@mantine/carousel';
import classes from './ExpertiseSection.module.css';
interface ExpertiseSectionProps {
title: string;
content: string;
images: { url: string; alt: string }[];
viewMoreLink: string;
imageOnLeft?: boolean;
}
const ExpertiseSection: React.FC<ExpertiseSectionProps> = ({
title,
content,
images,
viewMoreLink,
imageOnLeft = false,
}) => {
const slides = images.map((image) => (
<Carousel.Slide key={image.url}>
<Image src={image.url} alt={image.alt} style={{ width: '100%', height: 'auto', objectFit: 'cover' }} />
</Carousel.Slide>
));
return (
<Container>
<Grid className={classes.section}>
<Grid.Col order={imageOnLeft ? 2:1} span={{xs: 6, md: 8}} >
<Stack>
<Title>{title}</Title>
<Text size="lg">{content}</Text>
{/* <Button component="a" href={viewMoreLink}>
View More
</Button> */}
</Stack>
</Grid.Col>
<Grid.Col order={imageOnLeft ? 1:2} span={{xs: 6, md: 4}}>
<Carousel slideSize={300} align="start" slideGap="md" controlsOffset="xl" controlSize={28} loop withIndicators>
{slides}
</Carousel>
</Grid.Col>
</Grid>
</Container>
);
};
export default ExpertiseSection;

View File

@@ -0,0 +1,32 @@
.header {
height: 56px;
background-color: var(--mantine-color-body);
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.inner {
height: 56px;
display: flex;
justify-content: space-between;
align-items: center;
}
.link {
display: block;
line-height: 1;
padding: 8px 12px;
border-radius: var(--mantine-radius-sm);
text-decoration: none;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
font-size: var(--mantine-font-size-sm);
font-weight: 500;
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
[data-mantine-color-scheme] &[data-active] {
background-color: var(--mantine-color-blue-filled);
color: var(--mantine-color-white);
}
}

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { Burger, Container, Group } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { MantineLogo } from '@mantinex/mantine-logo';
import classes from './HeaderSimple.module.css';
import { ColorSchemeToggle } from '../ColorSchemeToggle/ColorSchemeToggle';
const links = [
{ link: '/about', label: 'Features' },
{ link: '/pricing', label: 'Pricing' },
{ link: '/learn', label: 'Learn' },
{ link: '/community', label: 'Community' },
];
export function HeaderSimple() {
const [opened, { toggle }] = useDisclosure(false);
const [active, setActive] = useState(links[0].link);
const items = links.map((link) => (
<a
key={link.label}
href={link.link}
className={classes.link}
data-active={active === link.link || undefined}
onClick={(event) => {
event.preventDefault();
setActive(link.link);
}}
>
{link.label}
</a>
));
return (
<header className={classes.header}>
<Container size="md" className={classes.inner}>
<MantineLogo size={28} />
<Group gap={5} visibleFrom="xs">
{items}
</Group>
<Burger opened={opened} onClick={toggle} hiddenFrom="xs" size="sm" />
<ColorSchemeToggle />
</Container>
</header>
);
}

View File

@@ -0,0 +1,63 @@
.wrapper {
position: relative;
box-sizing: border-box;
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.inner {
position: relative;
padding-top: 200px;
padding-bottom: 120px;
width: 80%;
@media (max-width: $mantine-breakpoint-sm) {
padding-bottom: 80px;
padding-top: 80px;
}
}
.title {
font-family:
Greycliff CF,
var(--mantine-font-family);
font-size: 62px;
font-weight: 900;
line-height: 1.1;
margin: 0;
padding: 0;
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
@media (max-width: $mantine-breakpoint-sm) {
font-size: 42px;
line-height: 1.2;
}
}
.description {
margin-top: var(--mantine-spacing-xl);
font-size: 24px;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 18px;
}
}
.controls {
margin-top: calc(var(--mantine-spacing-xl) * 2);
@media (max-width: $mantine-breakpoint-sm) {
margin-top: var(--mantine-spacing-xl);
}
}
.control {
height: 54px;
padding-left: 38px;
padding-right: 38px;
@media (max-width: $mantine-breakpoint-sm) {
height: 54px;
padding-left: 18px;
padding-right: 18px;
flex: 1;
}
}

View File

@@ -0,0 +1,45 @@
import { Button, Container, Group, Text } from '@mantine/core';
// import { GithubIcon } from '@mantinex/dev-icons';
import classes from './HeroTitle.module.css';
export function HeroTitle() {
return (
<div className={classes.wrapper}>
<Container fluid={true} size={700} className={classes.inner}>
<h1 className={classes.title}>
{' '}
<Text component="span" variant="gradient" gradient={{ from: 'blue', to: 'cyan' }} inherit>
Craig Macfadyen
</Text>{' '}
</h1>
<Text className={classes.description} c="dimmed">
Data Scientist with experience in consulting and research. Expertise in Computer Vision and Large Language Models. Take a look at what
I can do for your business and get in touch if you have any questions or you'd like to work together!
</Text>
<Group className={classes.controls}>
<Button
size="xl"
className={classes.control}
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
>
Get started
</Button>
<Button
component="a"
href="https://github.com/mantinedev/mantine"
size="xl"
variant="default"
className={classes.control}
// leftSection={<GithubIcon size={20} />}
>
GitHub
</Button>
</Group>
</Container>
</div>
);
}

View File

@@ -0,0 +1,4 @@
.card {
padding-left: 4rem; /* Add more padding to the left */
padding-right: 4rem; /* Add more padding to the right */
}

View File

@@ -0,0 +1,60 @@
import { Carousel } from '@mantine/carousel';
import { Card, Text, Avatar, Group, Stack, Title } from '@mantine/core';
import Autoplay from 'embla-carousel-autoplay';
import styles from './Testimonial.module.css';
const testimonials = [
{
text: "What set Craig apart was his ability to understand our business challenges and deliver a solution that worked for us. He didn't just build a model, he delivered a system our team could actually use.",
clientName: "Steven Adair",
role: "Director",
business: "Managing Utilities Limited",
},
];
function TestimonialsCarousel() {
const autoplay = Autoplay();
return (
<>
<Title align="center" mb="xl">
Testimonials
</Title>
<Carousel
slideSize={'100%'}
slideGap="md"
loop
align="start"
plugins={[autoplay]}
onMouseEnter={autoplay.stop}
onMouseLeave={autoplay.reset}
withControls
>
{testimonials.map((testimonial, index) => (
<Carousel.Slide key={index}>
<Card shadow="sm" padding="lg" radius="md" withBorder className={styles.card}>
<Stack gap="sm">
<Text size="lg">{testimonial.text}</Text>
<Group gap="sm">
<Avatar radius="xl">
{testimonial.clientName.charAt(0)}
</Avatar>
<div>
<Text size="lg" fw={700}>{testimonial.clientName}</Text>
<Text size="sm" c="dimmed">
{testimonial.role}, {testimonial.business}
</Text>
</div>
</Group>
</Stack>
</Card>
</Carousel.Slide>
))}
</Carousel>
</>
);
}
export default TestimonialsCarousel;

View File

@@ -0,0 +1,10 @@
.title {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: rem(100px);
font-weight: 900;
letter-spacing: rem(-2px);
@media (max-width: $mantine-breakpoint-md) {
font-size: rem(50px);
}
}

View File

@@ -0,0 +1,7 @@
import { Welcome } from './Welcome';
export default {
title: 'Welcome',
};
export const Usage = () => <Welcome />;

View File

@@ -0,0 +1,12 @@
import { render, screen } from '@test-utils';
import { Welcome } from './Welcome';
describe('Welcome component', () => {
it('has correct Vite guide link', () => {
render(<Welcome />);
expect(screen.getByText('this guide')).toHaveAttribute(
'href',
'https://mantine.dev/guides/vite/'
);
});
});

View File

@@ -0,0 +1,23 @@
import { Anchor, Text, Title } from '@mantine/core';
import classes from './Welcome.module.css';
export function Welcome() {
return (
<>
<Title className={classes.title} ta="center" mt={100}>
Welcome to{' '}
<Text inherit variant="gradient" component="span" gradient={{ from: 'pink', to: 'yellow' }}>
Mantine
</Text>
</Title>
<Text c="dimmed" ta="center" size="lg" maw={580} mx="auto" mt="xl">
This starter Vite project includes a minimal setup, if you want to learn more on Mantine +
Vite integration follow{' '}
<Anchor href="https://mantine.dev/guides/vite/" size="lg">
this guide
</Anchor>
. To get started edit pages/Home.page.tsx file.
</Text>
</>
);
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 163 163"><path fill="#339AF0" d="M162.162 81.5c0-45.011-36.301-81.5-81.08-81.5C36.301 0 0 36.489 0 81.5 0 126.51 36.301 163 81.081 163s81.081-36.49 81.081-81.5z"/><path fill="#fff" d="M65.983 43.049a6.234 6.234 0 00-.336 6.884 6.14 6.14 0 001.618 1.786c9.444 7.036 14.866 17.794 14.866 29.52 0 11.726-5.422 22.484-14.866 29.52a6.145 6.145 0 00-1.616 1.786 6.21 6.21 0 00-.694 4.693 6.21 6.21 0 001.028 2.186 6.151 6.151 0 006.457 2.319 6.154 6.154 0 002.177-1.035 50.083 50.083 0 007.947-7.39h17.493c3.406 0 6.174-2.772 6.174-6.194s-2.762-6.194-6.174-6.194h-9.655a49.165 49.165 0 004.071-19.69 49.167 49.167 0 00-4.07-19.692h9.66c3.406 0 6.173-2.771 6.173-6.194 0-3.422-2.762-6.193-6.173-6.193H82.574a50.112 50.112 0 00-7.952-7.397 6.15 6.15 0 00-4.578-1.153 6.189 6.189 0 00-4.055 2.438h-.006z"/><path fill="#fff" fill-rule="evenodd" d="M56.236 79.391a9.342 9.342 0 01.632-3.608 9.262 9.262 0 011.967-3.077 9.143 9.143 0 012.994-2.063 9.06 9.06 0 017.103 0 9.145 9.145 0 012.995 2.063 9.262 9.262 0 011.967 3.077 9.339 9.339 0 01-2.125 10.003 9.094 9.094 0 01-6.388 2.63 9.094 9.094 0 01-6.39-2.63 9.3 9.3 0 01-2.755-6.395z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
import ReactDOM from 'react-dom/client';
import App from './App';
import '@mantine/carousel/styles.css';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

View File

@@ -1,16 +1,20 @@
import React from 'react';
import ExpertiseSection from './ExpertiseSection';
import { HeroTitle } from '@/components/HeroTitle/HeroTitle';
import { Welcome } from '../components/Welcome/Welcome';
import { HeaderSimple } from '@/components/HeaderSimple/HeaderSimple';
import ExpertiseSection from '@/components/ExpertiseSection/ExpertiseSection';
import outputGif from '@/assets/output.gif';
import openWebUIGif from '@/assets/open-webui.gif';
import Testimonial from '@/components/Testimonial/Testimonial';
import { Container } from '@mantine/core';
import { ContactUs } from '@/components/ContactUs/ContactUs';
import { Contact } from '@/components/ContactUs/Contact';
const About = () => {
export function HomePage() {
return (
<section id="about" className="about py-20 bg-gray-100">
<div className="container mx-auto">
<p className="text-lg mb-8">
You have data, I have the skills to help you understand it.
</p>
<ExpertiseSection
<>
<HeaderSimple />
<HeroTitle />
<ExpertiseSection
title="Computer Vision"
content="Expertise in developing and deploying computer vision models for various applications, including object detection, image classification, and more. Ask me about state-of-the-art real-time models for your business."
images={[
@@ -19,7 +23,7 @@ const About = () => {
viewMoreLink="/computer-vision"
imageOnLeft={true}
/>
<ExpertiseSection
<ExpertiseSection
title="Natural Language Processing"
content="Building NLP models for tasks such as sentiment analysis, text classification, and language generation. If you need a chatbot for your business and care about privacy, or a simple text classification algorithm to understand your data, I can help."
images={[
@@ -28,9 +32,10 @@ const About = () => {
viewMoreLink="/nlp"
imageOnLeft={false}
/>
</div>
</section>
<Container mt={20}>
<Testimonial />
</Container>
<Contact />
</>
);
};
export default About;
}

View File

@@ -0,0 +1,5 @@
import { createTheme } from '@mantine/core';
export const theme = createTheme({
/** Put your mantine theme override here */
});

View File

@@ -0,0 +1,5 @@
import userEvent from '@testing-library/user-event';
export * from '@testing-library/react';
export { render } from './render';
export { userEvent };

View File

@@ -0,0 +1,11 @@
import { render as testingLibraryRender } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import { theme } from '../src/theme';
export function render(ui: React.ReactNode) {
return testingLibraryRender(ui, {
wrapper: ({ children }: { children: React.ReactNode }) => (
<MantineProvider theme={theme}>{children}</MantineProvider>
),
});
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"types": ["node", "@testing-library/jest-dom", "vitest/globals"],
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"],
"@test-utils": ["./test-utils"]
}
},
"include": ["src", "test-utils"]
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.mjs',
},
});

View File

@@ -0,0 +1,28 @@
import '@testing-library/jest-dom/vitest';
import { vi } from 'vitest';
const { getComputedStyle } = window;
window.getComputedStyle = (elt) => getComputedStyle(elt);
window.HTMLElement.prototype.scrollIntoView = () => {};
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
window.ResizeObserver = ResizeObserver;

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,71 +0,0 @@
# Setup process that worked:
```bash
npm create vite@latest
915 cd website/
916 npm install
917 npm run dev
918 npm install tailwindcss @tailwindcss/vite
919 npm run dev
920 npm i @types/node
921 npm run dev
922 npx shadcn@canary init
923 npm run dev
924 npm shadcn@canary add button
925 npx shadcn@canary add button
926 npm run dev
```
# Setup
TODO
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -1,28 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Craig Macfadyen</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +0,0 @@
{
"name": "website",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-slot": "^1.1.2",
"@tailwindcss/vite": "^4.0.8",
"@types/node": "^22.13.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^8.5.2",
"lucide-react": "^0.476.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.8",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,51 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
.App {
font-family: sans-serif;
}
section {
padding: 20px;
margin-bottom: 20px;
}

View File

@@ -1,22 +0,0 @@
import React from 'react';
import './App.css';
import Hero from './components/Hero';
import About from './components/About';
import Projects from './components/Projects';
import Contact from './components/Contact';
import Navbar04Page from './components/navbar-04/navbar-04';
import Testimonial06 from './components/testimonial-06/testimonial-06';
function App() {
return (
<div className="App min-h-screen bg-muted rounded-3xl">
<title>Home | Craig Macfadyen</title>
<Navbar04Page />
<Hero />
<About />
<Testimonial06 />
<Contact />
</div>
);
}
export default App;

View File

@@ -1,28 +0,0 @@
import { Button } from "@/components/ui/button";
import { SiLinkedin } from "react-icons/si";
const Contact = () => {
return (
<section id="contact" className="contact py-20">
<div className="container mx-auto">
<h2 className="text-3xl font-bold mb-4">Contact Me</h2>
<p className="text-lg mb-4">
Get in touch with me at <a href="mailto:cdmacfadyen@proton.me" className="text-blue-500">cdmacfadyen@proton.me</a>, or drop me a message on LinkedIn.
</p>
<div className="flex items-center justify-center gap-3">
<Button asChild>
<a href="mailto:cdmacfadyen@proton.me">Email Me</a>
</Button>
<Button asChild>
<a href="https://www.linkedin.com/in/craig-macfadyen-9a2041197/" target="_blank" rel="noopener noreferrer">
<SiLinkedin className="w-6 h-6 mr-2" />
LinkedIn
</a>
</Button>
</div>
</div>
</section>
);
};
export default Contact;

View File

@@ -1,38 +0,0 @@
import React from 'react';
import { Button } from "@/components/ui/button";
import ImageCarousel from './ImageCarousel';
interface ExpertiseSectionProps {
title: string;
content: string;
images: { url: string; alt: string }[];
viewMoreLink: string;
imageOnLeft?: boolean;
}
const ExpertiseSection: React.FC<ExpertiseSectionProps> = ({
title,
content,
images,
viewMoreLink,
imageOnLeft = false,
}) => {
return (
<section className="expertise-section py-20 bg-gray-100">
<div className={`container mx-auto flex flex-col md:flex-row items-center ${imageOnLeft ? 'md:flex-row-reverse' : ''}`}>
<div className="md:w-1/3 mb-8 mx-10 md:mb-0">
<ImageCarousel images={images} />
</div>
<div className="md:w-2/3 md:px-8">
<h2 className="text-3xl font-bold mb-4">{title}</h2>
<p className="text-lg mb-4">{content}</p>
{/* <Button asChild>
<a href={viewMoreLink}>View More</a>
</Button> */}
</div>
</div>
</section>
);
};
export default ExpertiseSection;

View File

@@ -1,18 +0,0 @@
import React from 'react';
import { Button } from "@/components/ui/button"
const Hero = () => {
return (
<section id="hero" className="hero">
<div className="container mx-auto text-center">
<h1 className="text-5xl font-bold mb-4">Craig Macfadyen</h1>
<p className="text-xl mb-8">
Data Scientist with experience in consulting and research. Expertise in Computer Vision and Large Language Models. Take a look at what
I can do for your business and get in touch if you have any questions or you'd like to work together!
</p>
</div>
</section>
);
};
export default Hero;

View File

@@ -1,43 +0,0 @@
import React from 'react';
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import Autoplay from "embla-carousel-autoplay";
interface ImageCarouselProps {
images: { url: string; alt: string }[];
delay?: number;
}
const ImageCarousel: React.FC<ImageCarouselProps> = ({ images, delay = 8000 }) => {
return (
<div className="relative">
<Carousel
plugins={[
Autoplay({
delay,
}),
]}
>
<CarouselContent>
{images.map((image, index) => (
<CarouselItem key={index}>
<img src={image.url} alt={image.alt} className="rounded-lg shadow-md" />
<div className="text-center py-2 rounded-lg bg-white">
{image.alt}
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
);
};
export default ImageCarousel;

View File

@@ -1,28 +0,0 @@
import React from 'react';
const Projects = () => {
return (
<section id="projects" className="projects py-20">
<div className="container mx-auto">
<h2 className="text-3xl font-bold mb-4">Projects</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{/* Example Project Card */}
<div className="bg-white rounded-lg shadow-md p-4">
<h3 className="text-xl font-semibold mb-2">Project 1</h3>
<p className="text-gray-700">Description of Project 1.</p>
</div>
<div className="bg-white rounded-lg shadow-md p-4">
<h3 className="text-xl font-semibold mb-2">Project 2</h3>
<p className="text-gray-700">Description of Project 2.</p>
</div>
<div className="bg-white rounded-lg shadow-md p-4">
<h3 className="text-xl font-semibold mb-2">Project 3</h3>
<p className="text-gray-700">Description of Project 3.</p>
</div>
</div>
</div>
</section>
);
};
export default Projects;

View File

@@ -1,19 +0,0 @@
export const Logo = () => (
<svg
id="logo-7"
width="124"
height="32"
viewBox="0 0 124 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M36.87 10.07H39.87V22.2H36.87V10.07ZM41.06 17.62C41.06 14.62 42.9 12.83 45.74 12.83C48.58 12.83 50.42 14.62 50.42 17.62C50.42 20.62 48.62 22.42 45.74 22.42C42.86 22.42 41.06 20.67 41.06 17.62ZM47.41 17.62C47.41 15.97 46.76 15 45.74 15C44.72 15 44.08 16 44.08 17.62C44.08 19.24 44.71 20.22 45.74 20.22C46.77 20.22 47.41 19.3 47.41 17.63V17.62ZM51.55 22.79H54.43C54.5671 23.0945 54.7988 23.3466 55.0907 23.5088C55.3826 23.6709 55.7191 23.7345 56.05 23.69C57.19 23.69 57.79 23.07 57.79 22.17V20.49H57.73C57.491 21.0049 57.1031 21.4363 56.6165 21.7287C56.1299 22.021 55.5668 22.1608 55 22.13C52.81 22.13 51.36 20.46 51.36 17.59C51.36 14.72 52.74 12.91 55.04 12.91C55.6246 12.8871 56.2022 13.0434 56.6955 13.3579C57.1888 13.6725 57.5742 14.1303 57.8 14.67V14.67V13H60.8V22.1C60.8 24.29 58.87 25.65 56.02 25.65C53.37 25.65 51.72 24.46 51.55 22.8V22.79ZM57.8 17.61C57.8 16.15 57.13 15.23 56.07 15.23C55.01 15.23 54.36 16.14 54.36 17.61C54.36 19.08 55 19.91 56.07 19.91C57.14 19.91 57.8 19.1 57.8 17.62V17.61ZM61.93 17.61C61.93 14.61 63.77 12.82 66.61 12.82C69.45 12.82 71.3 14.61 71.3 17.61C71.3 20.61 69.5 22.41 66.61 22.41C63.72 22.41 61.93 20.67 61.93 17.62V17.61ZM68.28 17.61C68.28 15.96 67.63 14.99 66.61 14.99C65.59 14.99 65 16 65 17.63C65 19.26 65.63 20.23 66.65 20.23C67.67 20.23 68.28 19.3 68.28 17.63V17.61ZM72.44 10.82C72.4321 10.5171 72.5144 10.2187 72.6763 9.96261C72.8383 9.70651 73.0726 9.50427 73.3496 9.38151C73.6266 9.25875 73.9338 9.221 74.2323 9.27305C74.5308 9.32511 74.8071 9.46462 75.0262 9.67389C75.2454 9.88317 75.3974 10.1528 75.4631 10.4486C75.5288 10.7444 75.5052 11.053 75.3952 11.3354C75.2853 11.6177 75.094 11.8611 74.8456 12.0346C74.5973 12.2081 74.3029 12.304 74 12.31C73.7992 12.3238 73.5977 12.2959 73.4082 12.2281C73.2186 12.1603 73.0452 12.0541 72.8987 11.916C72.7522 11.778 72.6358 11.6111 72.5569 11.4259C72.4779 11.2408 72.4381 11.0413 72.44 10.84V10.82ZM72.44 13.02H75.44V22.2H72.44V13.02ZM86.33 17.61C86.33 20.61 85 22.32 82.72 22.32C82.1354 22.3575 81.5533 22.2146 81.0525 21.9106C80.5517 21.6065 80.1564 21.156 79.92 20.62H79.86V25.14H76.86V13H79.86V14.64H79.92C80.1454 14.0951 80.5332 13.6329 81.0306 13.3162C81.528 12.9995 82.1109 12.8437 82.7 12.87C85 12.91 86.37 14.63 86.37 17.63L86.33 17.61ZM83.33 17.61C83.33 16.15 82.66 15.22 81.61 15.22C80.56 15.22 79.89 16.16 79.88 17.61C79.87 19.06 80.56 19.99 81.61 19.99C82.66 19.99 83.33 19.08 83.33 17.63V17.61ZM91.48 12.81C93.97 12.81 95.48 13.99 95.55 15.88H92.82C92.82 15.23 92.28 14.82 91.45 14.82C90.62 14.82 90.25 15.14 90.25 15.61C90.25 16.08 90.58 16.23 91.25 16.37L93.17 16.76C95 17.15 95.78 17.89 95.78 19.28C95.78 21.18 94.05 22.4 91.5 22.4C88.95 22.4 87.28 21.18 87.15 19.31H90.04C90.13 19.99 90.67 20.39 91.55 20.39C92.43 20.39 92.83 20.1 92.83 19.62C92.83 19.14 92.55 19.04 91.83 18.89L90.1 18.52C88.31 18.15 87.37 17.2 87.37 15.8C87.39 14 89 12.83 91.48 12.83V12.81ZM105.79 22.18H102.9V20.47H102.84C102.681 21.0441 102.331 21.5466 101.847 21.8941C101.363 22.2415 100.775 22.413 100.18 22.38C99.7242 22.4059 99.2682 22.3337 98.8427 22.1682C98.4172 22.0027 98.0322 21.7479 97.7137 21.4208C97.3952 21.0938 97.1505 20.7021 96.9964 20.2724C96.8422 19.8427 96.7821 19.3849 96.82 18.93V13H99.82V18.24C99.82 19.33 100.38 19.91 101.31 19.91C101.528 19.9104 101.744 19.8643 101.943 19.7746C102.141 19.6849 102.319 19.5537 102.463 19.3899C102.606 19.226 102.714 19.0333 102.777 18.8247C102.84 18.616 102.859 18.3962 102.83 18.18V13H105.83L105.79 22.18ZM107.24 13H110.14V14.77H110.2C110.359 14.2035 110.702 13.7057 111.174 13.3547C111.646 13.0037 112.222 12.8191 112.81 12.83C113.409 12.7821 114.003 12.9612 114.476 13.3318C114.948 13.7024 115.264 14.2372 115.36 14.83H115.42C115.601 14.2309 115.977 13.7093 116.488 13.3472C116.998 12.9851 117.615 12.8031 118.24 12.83C118.648 12.8163 119.054 12.8886 119.432 13.0422C119.811 13.1957 120.152 13.4272 120.435 13.7214C120.718 14.0157 120.936 14.3662 121.075 14.7501C121.213 15.134 121.27 15.5429 121.24 15.95V22.2H118.24V16.75C118.24 15.75 117.79 15.29 116.95 15.29C116.763 15.2884 116.577 15.327 116.406 15.4032C116.235 15.4794 116.082 15.5914 115.958 15.7317C115.834 15.872 115.741 16.0372 115.686 16.2163C115.631 16.3955 115.616 16.5843 115.64 16.77V22.2H112.79V16.71C112.79 15.79 112.34 15.29 111.52 15.29C111.331 15.2901 111.143 15.3303 110.971 15.408C110.798 15.4858 110.643 15.5993 110.518 15.741C110.392 15.8827 110.298 16.0495 110.241 16.2304C110.185 16.4112 110.167 16.6019 110.19 16.79V22.2H107.19L107.24 13Z"
className="fill-foreground"
/>
<path
d="M28.48 10.62C27.9711 9.45636 27.2976 8.37193 26.48 7.4C25.2715 5.92034 23.7633 4.71339 22.0547 3.8586C20.3461 3.00382 18.4758 2.52057 16.567 2.44066C14.6582 2.36075 12.7541 2.68599 10.98 3.39499C9.20597 4.10398 7.60217 5.18065 6.2742 6.55413C4.94622 7.9276 3.92417 9.56675 3.27532 11.3637C2.62647 13.1606 2.36552 15.0746 2.50966 16.9796C2.65381 18.8847 3.19976 20.7376 4.1116 22.4164C5.02344 24.0953 6.28049 25.562 7.80001 26.72C8.77501 27.4779 9.85236 28.094 11 28.55C12.609 29.2094 14.3311 29.549 16.07 29.55C19.6594 29.5421 23.0992 28.1113 25.6355 25.5713C28.1717 23.0313 29.5974 19.5894 29.6 16C29.6026 14.1485 29.2213 12.3166 28.48 10.62V10.62ZM16.06 5.18999C17.6216 5.18983 19.1643 5.53113 20.58 6.18999V6.18999C20.2348 6.33916 19.8718 6.44335 19.5 6.5C18.2766 6.67709 17.1433 7.24507 16.2692 8.11917C15.3951 8.99326 14.8271 10.1266 14.65 11.35C14.5723 12.0361 14.2602 12.6744 13.7665 13.1572C13.2728 13.64 12.6277 13.9376 11.94 14C10.7166 14.1771 9.58327 14.7451 8.70918 15.6192C7.83509 16.4933 7.2671 17.6266 7.09001 18.85C7.03005 19.5024 6.7517 20.1155 6.30001 20.59V20.59C5.52066 18.9433 5.17056 17.1261 5.28228 15.3077C5.394 13.4893 5.96391 11.7287 6.93898 10.1897C7.91404 8.65079 9.26258 7.38351 10.8591 6.50584C12.4556 5.62817 14.2482 5.16864 16.07 5.16999L16.06 5.18999ZM7.79001 23C7.91001 22.89 8.03001 22.79 8.15001 22.67C9.03966 21.8075 9.61072 20.6689 9.77001 19.44C9.83459 18.7492 10.143 18.104 10.64 17.62C11.1183 17.1222 11.762 16.8163 12.45 16.76C13.6734 16.5829 14.8067 16.0149 15.6808 15.1408C16.5549 14.2667 17.1229 13.1334 17.3 11.91C17.3433 11.1875 17.6533 10.5068 18.17 10C18.6601 9.51185 19.3099 9.2171 20 9.16999C21.1239 9.01536 22.1721 8.51571 23 7.74C23.9427 8.52207 24.7413 9.46289 25.36 10.52C25.322 10.5713 25.2784 10.6183 25.23 10.66C24.7527 11.1622 24.1098 11.4748 23.42 11.54C22.1953 11.714 21.0603 12.281 20.1856 13.1556C19.311 14.0303 18.744 15.1653 18.57 16.39C18.4995 17.0784 18.1932 17.7213 17.703 18.2097C17.2127 18.6982 16.5687 19.0021 15.88 19.07C14.653 19.2457 13.5155 19.8126 12.6363 20.6863C11.7572 21.5601 11.1833 22.6941 11 23.92C10.9462 24.4087 10.7783 24.878 10.51 25.29C9.484 24.6808 8.5651 23.9072 7.79001 23V23ZM16.06 26.86C15.0453 26.8611 14.0354 26.7197 13.06 26.44C13.3937 25.818 13.6106 25.1401 13.7 24.44C13.7701 23.7531 14.075 23.1114 14.5632 22.6232C15.0514 22.135 15.6931 21.8301 16.38 21.76C17.6052 21.5849 18.7408 21.0178 19.6169 20.1435C20.4929 19.2693 21.0624 18.1348 21.24 16.91C21.3101 16.2231 21.615 15.5814 22.1032 15.0932C22.5914 14.605 23.2331 14.3001 23.92 14.23C24.842 14.1101 25.7208 13.7668 26.48 13.23C26.9016 14.8279 26.9515 16.5011 26.626 18.1213C26.3005 19.7415 25.6081 21.2657 24.6021 22.5768C23.5961 23.8878 22.3032 24.9511 20.8224 25.6849C19.3417 26.4187 17.7126 26.8036 16.06 26.81V26.86Z"
className="fill-foreground"
/>
</svg>
);

View File

@@ -1,29 +0,0 @@
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
} from "@/components/ui/navigation-menu";
import { NavigationMenuProps } from "@radix-ui/react-navigation-menu";
export const NavMenu = (props: NavigationMenuProps) => (
<NavigationMenu {...props}>
<NavigationMenuList className="gap-6 space-x-0 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-start">
<NavigationMenuItem>
<NavigationMenuLink asChild>
<a href="/">Home</a>
</NavigationMenuLink>
</NavigationMenuItem>
{/* <NavigationMenuItem>
<NavigationMenuLink asChild>
<a href="#">Blog</a>
</NavigationMenuLink>
</NavigationMenuItem> */}
<NavigationMenuItem>
<NavigationMenuLink asChild>
<a href="#contact">Contact</a>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);

View File

@@ -1,42 +0,0 @@
import { Logo } from "./logo";
import { NavMenu } from "./nav-menu";
import { NavigationSheet } from "./navigation-sheet";
import { SiLinkedin, SiGithub, SiGitea } from "react-icons/si";
const Navbar04Page = () => {
return (
<nav className="top-6 inset-x-4 h-16 bg-background border dark:border-slate-700/70 max-w-screen-xl mx-auto rounded-full">
<div className="h-full flex items-center justify-between mx-auto px-4">
{/* Desktop Menu */}
<NavMenu className="hidden md:block" />
<div className="flex items-center gap-3">
{/* Social Media Links */}
<div className="flex items-center gap-3">
<a href="https://www.linkedin.com/in/craig-macfadyen-9a2041197/" target="_blank" rel="noopener noreferrer">
<SiLinkedin className="w-6 h-6 text-gray-700 dark:text-gray-300" />
</a>
<a href="https://github.com/cdmacfadyen" target="_blank" rel="noopener noreferrer">
<SiGithub className="w-6 h-6 text-gray-700 dark:text-gray-300" />
</a>
<a href="https://gitea.craigmacfadyen.co.uk/" target="_blank" rel="noopener noreferrer">
<SiGitea className="w-6 h-6 text-gray-700 dark:text-gray-300" />
</a>
</div>
{/* <Button
variant="outline"
className="hidden sm:inline-flex rounded-full"
>
Sign In
</Button> */}
{/* Mobile Menu */}
<div className="md:hidden">
<NavigationSheet />
</div>
</div>
</div>
</nav>
);
};
export default Navbar04Page;

View File

@@ -1,21 +0,0 @@
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Menu } from "lucide-react";
import { Logo } from "./logo";
import { NavMenu } from "./nav-menu";
export const NavigationSheet = () => {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="icon" className="rounded-full">
<Menu />
</Button>
</SheetTrigger>
<SheetContent>
<Logo />
<NavMenu orientation="vertical" className="mt-12" />
</SheetContent>
</Sheet>
);
};

View File

@@ -1,119 +0,0 @@
"use client";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Carousel,
CarouselApi,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { cn } from "@/lib/utils";
import { useEffect, useState } from "react";
const testimonials = [
{
id: 1,
name: "Steven Adair",
designation: "Director",
company: "Managing Utilities Ltd",
testimonial:
"What set Craig apart was his ability to understand our business challenges and deliver a solution that worked for us. He didn't just build a model, he delivered a system our team could actually use."
},
];
const Testimonial06 = () => {
const [api, setApi] = useState<CarouselApi>();
const [current, setCurrent] = useState(0);
const [count, setCount] = useState(0);
useEffect(() => {
if (!api) {
return;
}
setCount(api.scrollSnapList().length);
setCurrent(api.selectedScrollSnap() + 1);
api.on("select", () => {
setCurrent(api.selectedScrollSnap() + 1);
});
}, [api]);
return (
<div className="w-full flex justify-center items-center px-6">
<div className="w-full">
<h2 className="mb-14 text-5xl md:text-6xl font-bold text-center tracking-tight">
Testimonials
</h2>
<div className="container w-full lg:max-w-screen-lg xl:max-w-screen-xl mx-auto px-12">
<Carousel setApi={setApi}>
<CarouselContent>
{testimonials.map((testimonial) => (
<CarouselItem key={testimonial.id}>
<TestimonialCard testimonial={testimonial} />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
{/* <div className="flex items-center justify-center gap-2">
{Array.from({ length: count }).map((_, index) => (
<button
key={index}
onClick={() => api?.scrollTo(index)}
className={cn("h-3.5 w-3.5 rounded-full border-2", {
"bg-primary border-primary": current === index + 1,
})}
/>
))}
</div> */}
</div>
</div>
</div>
);
};
const TestimonialCard = ({
testimonial,
}: {
testimonial: (typeof testimonials)[number];
}) => (
<div className="mb-8 bg-accent rounded-xl py-8 px-6 sm:py-6">
<div className="flex items-center justify-between gap-20">
<div className="flex flex-col justify-center">
<div className="flex items-center justify-between gap-1">
<div className="hidden sm:flex md:hidden items-center gap-4">
<Avatar className="w-8 h-8 md:w-10 md:h-10">
<AvatarFallback className="text-xl font-medium bg-primary text-primary-foreground">
{testimonial.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<p className="text-lg font-semibold">{testimonial.name}</p>
<p className="text-sm text-gray-500">{testimonial.designation}</p>
</div>
</div>
</div>
<p className="mt-6 text-lg sm:text-2xl lg:text-[1.75rem] xl:text-3xl leading-normal lg:!leading-normal font-semibold tracking-tight">
&quot;{testimonial.testimonial}&quot;
</p>
<div className="flex sm:hidden md:flex mt-6 items-center gap-4 justify-center">
<Avatar className="w-12 h-12 md:w-14 md:h-14">
<AvatarFallback className="text-3xl font-bold bg-primary text-primary-foreground">
{testimonial.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<p className="text-lg font-semibold">{testimonial.name}</p>
<p className="text-sm text-gray-500">{testimonial.designation}</p>
<p className="text-sm font-semibold">{testimonial.company}</p>
</div>
</div>
</div>
</div>
</div>
);
export default Testimonial06;

View File

@@ -1,51 +0,0 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -1,58 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
outline:
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button }

View File

@@ -1,241 +0,0 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View File

@@ -1,168 +0,0 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
)}
{...props}
/>
)
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
)
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[active=true]:bg-accent/50 data-[state=open]:bg-accent/50 data-[active=true]:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
)
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
)
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
/>
)
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
)
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}

View File

@@ -1,139 +0,0 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -1,192 +0,0 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.87 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.87 0 0);
}
a {
font-weight: 500;
/* color: #646cff; */
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
color: #213547;
background-color: #f9f9f9;
}
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -1,10 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -1,31 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

View File

@@ -1,13 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -1,24 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,17 +0,0 @@
import path from "path"
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})