move ot mantine frontend
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.section {
|
||||
margin-top: 80px;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.card {
|
||||
padding-left: 4rem; /* Add more padding to the left */
|
||||
padding-right: 4rem; /* Add more padding to the right */
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Welcome } from './Welcome';
|
||||
|
||||
export default {
|
||||
title: 'Welcome',
|
||||
};
|
||||
|
||||
export const Usage = () => <Welcome />;
|
||||
@@ -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/'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user