first commit
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Adaptivework } from './components/Adaptivework';
|
||||
import { CTA } from './components/CTA';
|
||||
import { Footer } from './components/Footer';
|
||||
import { Hero } from './components/Hero';
|
||||
import { Navbar } from './components/Navbar';
|
||||
import { Process } from './components/Process';
|
||||
import { Services } from './components/Services';
|
||||
import { WhyUs } from './components/WhyUs';
|
||||
import { useTheme } from './hooks/useTheme';
|
||||
|
||||
export default function App() {
|
||||
const { i18n } = useTranslation();
|
||||
const { isDark, toggle } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const lang = (i18n.resolvedLanguage ?? i18n.language ?? 'pt').slice(0, 2);
|
||||
document.documentElement.lang = lang;
|
||||
}, [i18n.resolvedLanguage, i18n.language]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href="#main"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[100] focus:rounded-md focus:bg-secondary focus:px-4 focus:py-2 focus:text-secondary-fg"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
<Navbar isDark={isDark} onToggleTheme={toggle} />
|
||||
<main id="main">
|
||||
<Hero />
|
||||
<Services />
|
||||
<Adaptivework />
|
||||
<WhyUs />
|
||||
<Process />
|
||||
<CTA />
|
||||
</main>
|
||||
<Footer isDark={isDark} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ArrowRight,
|
||||
GraduationCap,
|
||||
Plug,
|
||||
Settings2,
|
||||
Sparkles,
|
||||
Wand2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type FeatureKey = 'implementation' | 'customization' | 'integration' | 'training';
|
||||
|
||||
const FEATURES: { key: FeatureKey; Icon: LucideIcon }[] = [
|
||||
{ key: 'implementation', Icon: Settings2 },
|
||||
{ key: 'customization', Icon: Wand2 },
|
||||
{ key: 'integration', Icon: Plug },
|
||||
{ key: 'training', Icon: GraduationCap },
|
||||
];
|
||||
|
||||
export function Adaptivework() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section id="adaptivework" className="section relative overflow-hidden">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 bg-radial-fade opacity-60"
|
||||
/>
|
||||
|
||||
<div className="container-rl relative grid gap-12 lg:grid-cols-[0.95fr_1.05fr] lg:items-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
<span className="badge">
|
||||
<Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t('adaptivework.badge')}
|
||||
</span>
|
||||
<h2 className="heading text-balance mt-5 text-3xl md:text-4xl">
|
||||
{t('adaptivework.title')}
|
||||
</h2>
|
||||
<p className="mt-4 text-fg-muted text-balance md:text-lg">
|
||||
{t('adaptivework.subtitle')}
|
||||
</p>
|
||||
|
||||
<a href="#contact" className="btn-primary mt-8">
|
||||
{t('adaptivework.cta')}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{FEATURES.map(({ key, Icon }, index) => (
|
||||
<motion.div
|
||||
key={key}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: 0.08 + index * 0.07,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
className="card card-hover"
|
||||
>
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-secondary text-secondary-fg">
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
<h3 className="heading mt-4 text-lg">
|
||||
{t(`adaptivework.features.${key}.title`)}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-fg-muted leading-relaxed">
|
||||
{t(`adaptivework.features.${key}.description`)}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight, Mail, Phone } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function CTA() {
|
||||
const { t } = useTranslation();
|
||||
const email = t('cta.email');
|
||||
const whatsapp = t('cta.whatsapp');
|
||||
|
||||
return (
|
||||
<section id="contact" className="section">
|
||||
<div className="container-rl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
|
||||
className="relative overflow-hidden rounded-2xl bg-secondary text-secondary-fg shadow-lg"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -top-32 -right-32 h-[420px] w-[420px] rounded-full bg-primary/30 blur-3xl"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -bottom-40 -left-32 h-[380px] w-[380px] rounded-full bg-primary/20 blur-3xl"
|
||||
/>
|
||||
|
||||
<div className="relative grid gap-10 p-10 lg:grid-cols-[1.1fr_0.9fr] lg:p-16">
|
||||
<div>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/20 px-3 py-1 text-xs font-semibold text-primary">
|
||||
{t('cta.eyebrow')}
|
||||
</span>
|
||||
<h2 className="heading text-balance mt-5 text-3xl text-secondary-fg md:text-4xl">
|
||||
{t('cta.title')}
|
||||
</h2>
|
||||
<p className="mt-4 text-secondary-fg/80 md:text-lg">
|
||||
{t('cta.subtitle')}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={`mailto:${email}`}
|
||||
className="btn-primary mt-8"
|
||||
>
|
||||
{t('cta.ctaPrimary')}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 self-center">
|
||||
<ContactCard
|
||||
Icon={Mail}
|
||||
label={t('cta.emailLabel')}
|
||||
value={email}
|
||||
href={`mailto:${email}`}
|
||||
/>
|
||||
<ContactCard
|
||||
Icon={Phone}
|
||||
label={t('cta.whatsappLabel')}
|
||||
value={whatsapp}
|
||||
href={`https://wa.me/${whatsapp.replace(/\D/g, '')}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ContactCard({
|
||||
Icon,
|
||||
label,
|
||||
value,
|
||||
href,
|
||||
}: {
|
||||
Icon: typeof Mail;
|
||||
label: string;
|
||||
value: string;
|
||||
href: string;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="group flex items-center gap-4 rounded-xl border border-white/10 bg-white/5 p-5 transition-colors hover:bg-white/10"
|
||||
>
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-primary text-primary-fg">
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs uppercase tracking-wider text-secondary-fg/60">
|
||||
{label}
|
||||
</div>
|
||||
<div className="truncate font-semibold text-secondary-fg group-hover:underline">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Github, Linkedin, Mail } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { rivusLogoSrc } from '../lib/logos';
|
||||
import { NAV_SECTIONS } from '../lib/sections';
|
||||
|
||||
const SOCIALS = [
|
||||
{ Icon: Linkedin, href: 'https://www.linkedin.com', label: 'LinkedIn' },
|
||||
{ Icon: Github, href: 'https://github.com', label: 'GitHub' },
|
||||
{ Icon: Mail, href: 'mailto:contato@rivuslab.com', label: 'Email' },
|
||||
] as const;
|
||||
|
||||
export function Footer({ isDark }: { isDark: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="border-t border-border bg-bg-muted">
|
||||
<div className="container-rl py-14">
|
||||
<div className="grid gap-10 md:grid-cols-[1.4fr_1fr_1fr]">
|
||||
<div>
|
||||
<img
|
||||
src={rivusLogoSrc(isDark)}
|
||||
alt="RivusLab"
|
||||
className="h-9 w-auto"
|
||||
width={160}
|
||||
height={36}
|
||||
/>
|
||||
<p className="mt-4 max-w-sm text-sm text-fg-muted leading-relaxed">
|
||||
{t('footer.tagline')}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex items-center gap-2">
|
||||
{SOCIALS.map(({ Icon, href, label }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={label}
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-border bg-bg text-fg-muted transition-colors hover:border-primary hover:text-primary"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FooterColumn title={t('footer.navigation')}>
|
||||
{NAV_SECTIONS.map((section) => (
|
||||
<li key={section.id}>
|
||||
<a
|
||||
href={`#${section.id}`}
|
||||
className="text-sm text-fg-muted transition-colors hover:text-fg"
|
||||
>
|
||||
{t(section.labelKey)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</FooterColumn>
|
||||
|
||||
<FooterColumn title={t('footer.contact')}>
|
||||
<li>
|
||||
<a
|
||||
href="mailto:contato@rivuslab.com"
|
||||
className="text-sm text-fg-muted transition-colors hover:text-fg"
|
||||
>
|
||||
contato@rivuslab.com
|
||||
</a>
|
||||
</li>
|
||||
<li className="text-sm text-fg-muted">+55 (00) 00000-0000</li>
|
||||
</FooterColumn>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 flex flex-col items-start justify-between gap-3 border-t border-border pt-6 text-xs text-fg-muted md:flex-row md:items-center">
|
||||
<span>© {year} RivusLab. {t('footer.rights')}</span>
|
||||
<span className="font-mono">v0.1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterColumn({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-xs font-bold uppercase tracking-[0.18em] text-fg">
|
||||
{title}
|
||||
</h4>
|
||||
<ul className="mt-4 space-y-2.5">{children}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight, Sparkles } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function Hero() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section
|
||||
id="top"
|
||||
className="relative overflow-hidden pt-32 pb-20 md:pt-40 md:pb-28"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="bg-radial-fade pointer-events-none absolute inset-0"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="bg-grid pointer-events-none absolute inset-0 opacity-[0.35] [mask-image:radial-gradient(ellipse_at_center,black_30%,transparent_70%)]"
|
||||
/>
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -top-32 -right-32 h-[480px] w-[480px] rounded-full bg-primary/30 blur-3xl"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -bottom-40 -left-24 h-[420px] w-[420px] rounded-full bg-secondary/20 blur-3xl"
|
||||
/>
|
||||
|
||||
<div className="container-rl relative grid items-center gap-14 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
|
||||
className="max-w-2xl"
|
||||
>
|
||||
<span className="badge">
|
||||
<Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t('hero.eyebrow')}
|
||||
</span>
|
||||
|
||||
<h1 className="heading text-balance mt-6 text-4xl leading-[1.05] sm:text-5xl md:text-6xl">
|
||||
{t('hero.title')}
|
||||
</h1>
|
||||
|
||||
<p className="mt-6 max-w-xl text-lg text-fg-muted text-balance">
|
||||
{t('hero.subtitle')}
|
||||
</p>
|
||||
|
||||
<div className="mt-9 flex flex-wrap items-center gap-3">
|
||||
<a href="#contact" className="btn-primary">
|
||||
{t('hero.ctaPrimary')}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<a href="#services" className="btn-ghost">
|
||||
{t('hero.ctaSecondary')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* <dl className="mt-12 grid max-w-lg grid-cols-3 gap-6 border-t border-border pt-8">
|
||||
{STATS.map((stat) => (
|
||||
<div key={stat.key}>
|
||||
<dt className="text-xs uppercase tracking-wider text-fg-muted">
|
||||
{t(`hero.stats.${stat.key}`)}
|
||||
</dt>
|
||||
<dd className="mt-1 font-display text-2xl font-bold text-fg md:text-3xl">
|
||||
{stat.value}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl> */}
|
||||
</motion.div>
|
||||
|
||||
{/* <motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.7, delay: 0.15, ease: [0.4, 0, 0.2, 1] }}
|
||||
className="relative hidden lg:block"
|
||||
>
|
||||
<HeroVisual />
|
||||
</motion.div> */}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Globe } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SUPPORTED_LANGUAGES, type SupportedLanguage } from '../i18n';
|
||||
|
||||
const LABELS: Record<SupportedLanguage, string> = {
|
||||
pt: 'PT',
|
||||
en: 'EN',
|
||||
es: 'ES',
|
||||
};
|
||||
|
||||
export function LanguageSwitcher({ className = '' }: { className?: string }) {
|
||||
const { i18n, t } = useTranslation();
|
||||
const current = (i18n.resolvedLanguage ?? i18n.language ?? 'pt').slice(0, 2) as SupportedLanguage;
|
||||
|
||||
const handleChange = (lang: SupportedLanguage) => {
|
||||
if (lang === current) return;
|
||||
void i18n.changeLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inline-flex items-center gap-1 rounded-full border border-border bg-bg p-1 text-xs font-semibold ${className}`}
|
||||
role="group"
|
||||
aria-label={t('lang.switchTo')}
|
||||
>
|
||||
<Globe
|
||||
className="ml-1.5 mr-0.5 h-3.5 w-3.5 text-fg-muted"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{SUPPORTED_LANGUAGES.map((lang) => {
|
||||
const active = lang === current;
|
||||
return (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
onClick={() => handleChange(lang)}
|
||||
aria-pressed={active}
|
||||
className={[
|
||||
'rounded-full px-2.5 py-1 transition-colors',
|
||||
active
|
||||
? 'bg-secondary text-secondary-fg'
|
||||
: 'text-fg-muted hover:text-fg',
|
||||
].join(' ')}
|
||||
>
|
||||
{LABELS[lang]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { rivusLogoSrc } from '../lib/logos';
|
||||
import { NAV_SECTIONS } from '../lib/sections';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
|
||||
export function Navbar({
|
||||
isDark,
|
||||
onToggleTheme,
|
||||
}: {
|
||||
isDark: boolean;
|
||||
onToggleTheme: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 8);
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const closeMenu = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={[
|
||||
'fixed inset-x-0 top-0 z-50 transition-all duration-300 ease-smooth',
|
||||
scrolled
|
||||
? 'border-b border-border/80 bg-bg/85 backdrop-blur-md shadow-sm'
|
||||
: 'border-b border-transparent bg-transparent',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="container-rl flex h-16 items-center justify-between gap-4 md:h-20">
|
||||
<a href="#top" className="flex items-center gap-2" aria-label="RivusLab">
|
||||
<img
|
||||
src={rivusLogoSrc(isDark)}
|
||||
alt="RivusLab"
|
||||
className="h-8 w-auto md:h-9"
|
||||
width={160}
|
||||
height={36}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<nav
|
||||
className="hidden items-center gap-7 lg:flex"
|
||||
aria-label="Primary"
|
||||
>
|
||||
{NAV_SECTIONS.map((section) => (
|
||||
<a
|
||||
key={section.id}
|
||||
href={`#${section.id}`}
|
||||
className="text-sm font-medium text-fg-muted transition-colors hover:text-fg"
|
||||
>
|
||||
{t(section.labelKey)}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="hidden items-center gap-3 lg:flex">
|
||||
<ThemeToggle isDark={isDark} onToggle={onToggleTheme} />
|
||||
<LanguageSwitcher />
|
||||
<a href="#contact" className="btn-primary">
|
||||
{t('nav.cta')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-md border border-border bg-bg text-fg lg:hidden"
|
||||
aria-label={open ? t('nav.closeMenu') : t('nav.openMenu')}
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={[
|
||||
'lg:hidden overflow-hidden transition-[max-height,opacity] duration-300 ease-smooth',
|
||||
open
|
||||
? 'max-h-[80vh] opacity-100 border-t border-border bg-bg'
|
||||
: 'max-h-0 opacity-0',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="container-rl flex flex-col gap-1 py-4">
|
||||
{NAV_SECTIONS.map((section) => (
|
||||
<a
|
||||
key={section.id}
|
||||
href={`#${section.id}`}
|
||||
onClick={closeMenu}
|
||||
className="rounded-md px-3 py-3 text-base font-medium text-fg hover:bg-bg-muted"
|
||||
>
|
||||
{t(section.labelKey)}
|
||||
</a>
|
||||
))}
|
||||
<div className="mt-2 flex items-center justify-between gap-3 px-3 pt-3 border-t border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<ThemeToggle isDark={isDark} onToggle={onToggleTheme} />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<a
|
||||
href="#contact"
|
||||
onClick={closeMenu}
|
||||
className="btn-primary flex-1 text-center"
|
||||
>
|
||||
{t('nav.cta')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SectionHeader } from './Services';
|
||||
|
||||
const STEPS = ['diagnose', 'propose', 'execute', 'evolve'] as const;
|
||||
|
||||
export function Process() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section id="process" className="section bg-bg-muted">
|
||||
<div className="container-rl">
|
||||
<SectionHeader
|
||||
eyebrow={t('process.eyebrow')}
|
||||
title={t('process.title')}
|
||||
/>
|
||||
|
||||
<ol className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{STEPS.map((step, index) => (
|
||||
<motion.li
|
||||
key={step}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: index * 0.08,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
className="relative"
|
||||
>
|
||||
<div className="card h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-secondary font-display text-sm font-bold text-secondary-fg">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
{index < STEPS.length - 1 && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="hidden h-px flex-1 ml-4 bg-gradient-to-r from-primary/60 to-transparent lg:block"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="heading mt-5 text-lg">
|
||||
{t(`process.steps.${step}.title`)}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-fg-muted leading-relaxed">
|
||||
{t(`process.steps.${step}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { Code2, Compass, type LucideIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type ServiceKey = 'outsourcing' | 'squads' | 'consulting';
|
||||
|
||||
const SERVICES: { key: ServiceKey; Icon: LucideIcon }[] = [
|
||||
{ key: 'outsourcing', Icon: Code2 },
|
||||
// { key: 'squads', Icon: Users },
|
||||
{ key: 'consulting', Icon: Compass },
|
||||
];
|
||||
|
||||
export function Services() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section id="services" className="section bg-bg-muted">
|
||||
<div className="container-rl">
|
||||
<SectionHeader
|
||||
eyebrow={t('services.eyebrow')}
|
||||
title={t('services.title')}
|
||||
subtitle={t('services.subtitle')}
|
||||
/>
|
||||
|
||||
<div className="mt-14 grid gap-6 md:grid-cols-3">
|
||||
{SERVICES.map(({ key, Icon }, index) => (
|
||||
<motion.article
|
||||
key={key}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: index * 0.08,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
className="card card-hover group"
|
||||
>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-secondary text-secondary-fg transition-transform group-hover:scale-110">
|
||||
<Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</div>
|
||||
<h3 className="heading mt-5 text-xl">
|
||||
{t(`services.items.${key}.title`)}
|
||||
</h3>
|
||||
<p className="mt-3 text-fg-muted leading-relaxed">
|
||||
{t(`services.items.${key}.description`)}
|
||||
</p>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function SectionHeader({
|
||||
eyebrow,
|
||||
title,
|
||||
subtitle,
|
||||
align = 'center',
|
||||
}: {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
align?: 'center' | 'left';
|
||||
}) {
|
||||
const alignClass =
|
||||
align === 'center' ? 'mx-auto text-center' : 'text-left';
|
||||
return (
|
||||
<div className={`max-w-2xl ${alignClass}`}>
|
||||
{eyebrow && (
|
||||
<span className="text-xs font-bold uppercase tracking-[0.18em] text-primary">
|
||||
{eyebrow}
|
||||
</span>
|
||||
)}
|
||||
<h2 className="heading text-balance mt-3 text-3xl md:text-4xl">{title}</h2>
|
||||
{subtitle && (
|
||||
<p className="mt-4 text-fg-muted text-balance md:text-lg">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function ThemeToggle({
|
||||
isDark,
|
||||
onToggle,
|
||||
className = '',
|
||||
}: {
|
||||
isDark: boolean;
|
||||
onToggle: () => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className={[
|
||||
'inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-border bg-bg text-fg transition-colors hover:bg-bg-muted',
|
||||
className,
|
||||
].join(' ')}
|
||||
aria-label={isDark ? t('theme.switchToLight') : t('theme.switchToDark')}
|
||||
>
|
||||
{isDark ? (
|
||||
<Sun className="h-5 w-5" aria-hidden="true" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
MessageCircle,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Target,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SectionHeader } from './Services';
|
||||
|
||||
type Key = 'senior' | 'delivery' | 'communication' | 'quality';
|
||||
|
||||
const PILLARS: { key: Key; Icon: LucideIcon }[] = [
|
||||
{ key: 'senior', Icon: Sparkles },
|
||||
{ key: 'delivery', Icon: Target },
|
||||
{ key: 'communication', Icon: MessageCircle },
|
||||
{ key: 'quality', Icon: ShieldCheck },
|
||||
];
|
||||
|
||||
export function WhyUs() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section id="whyus" className="section">
|
||||
<div className="container-rl">
|
||||
<SectionHeader
|
||||
eyebrow={t('whyus.eyebrow')}
|
||||
title={t('whyus.title')}
|
||||
/>
|
||||
|
||||
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{PILLARS.map(({ key, Icon }, index) => (
|
||||
<motion.div
|
||||
key={key}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: index * 0.07,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
className="group relative rounded-xl border border-border bg-bg p-6 transition-all duration-300 hover:-translate-y-1 hover:border-primary/50 hover:shadow-md"
|
||||
>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-fg transition-transform group-hover:scale-110">
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
<h3 className="heading mt-5 text-lg">
|
||||
{t(`whyus.items.${key}.title`)}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-fg-muted leading-relaxed">
|
||||
{t(`whyus.items.${key}.description`)}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
applyTheme,
|
||||
getStoredTheme,
|
||||
readEffectiveDark,
|
||||
resolveDark,
|
||||
toggleStoredTheme,
|
||||
} from '../lib/theme';
|
||||
|
||||
export function useTheme(): { isDark: boolean; toggle: () => void } {
|
||||
const [isDark, setIsDark] = useState(() => readEffectiveDark());
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const sync = () => {
|
||||
const stored = getStoredTheme();
|
||||
const dark = resolveDark(stored, mq.matches);
|
||||
applyTheme(dark);
|
||||
setIsDark(dark);
|
||||
};
|
||||
|
||||
sync();
|
||||
|
||||
const onPreferenceChange = () => {
|
||||
if (getStoredTheme() !== null) return;
|
||||
const dark = resolveDark(null, mq.matches);
|
||||
applyTheme(dark);
|
||||
setIsDark(dark);
|
||||
};
|
||||
|
||||
mq.addEventListener('change', onPreferenceChange);
|
||||
return () => mq.removeEventListener('change', onPreferenceChange);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
const nextDark = toggleStoredTheme();
|
||||
setIsDark(nextDark);
|
||||
}, []);
|
||||
|
||||
return { isDark, toggle };
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import i18n from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import en from './locales/en.json';
|
||||
import es from './locales/es.json';
|
||||
import pt from './locales/pt.json';
|
||||
|
||||
export const SUPPORTED_LANGUAGES = ['pt', 'en', 'es'] as const;
|
||||
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
||||
|
||||
void i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
pt: { translation: pt },
|
||||
en: { translation: en },
|
||||
es: { translation: es },
|
||||
},
|
||||
fallbackLng: 'pt',
|
||||
supportedLngs: SUPPORTED_LANGUAGES,
|
||||
interpolation: { escapeValue: false },
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator', 'htmlTag'],
|
||||
caches: ['localStorage'],
|
||||
lookupLocalStorage: 'rivuslab-lang',
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"nav": {
|
||||
"services": "Services",
|
||||
"adaptivework": "AdaptiveWork",
|
||||
"process": "Process",
|
||||
"whyus": "Why us",
|
||||
"contact": "Contact",
|
||||
"cta": "Get in touch",
|
||||
"openMenu": "Open menu",
|
||||
"closeMenu": "Close menu"
|
||||
},
|
||||
"theme": {
|
||||
"switchToLight": "Switch to light theme",
|
||||
"switchToDark": "Switch to dark theme"
|
||||
},
|
||||
"hero": {
|
||||
"eyebrow": "Software engineering & Planview AdaptiveWork",
|
||||
"title": "Custom software engineering for your business",
|
||||
"subtitle": "We are a boutique software studio. We deliver senior teams, maintainable code and specialized support on Planview AdaptiveWork to accelerate your results.",
|
||||
"ctaPrimary": "Get in touch",
|
||||
"ctaSecondary": "Explore our services",
|
||||
"stats": {
|
||||
"experience": "years of experience",
|
||||
"projects": "delivered projects",
|
||||
"uptime": "customer satisfaction"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"eyebrow": "Services",
|
||||
"title": "End-to-end solutions for your software lifecycle",
|
||||
"subtitle": "From concept to continuous evolution, we operate as an extension of your team.",
|
||||
"items": {
|
||||
"outsourcing": {
|
||||
"title": "Development Outsourcing",
|
||||
"description": "On-demand developers and full teams with technical leadership and a delivery mindset."
|
||||
},
|
||||
"squads": {
|
||||
"title": "Dedicated Squads",
|
||||
"description": "Cross-functional teams (dev, QA, design, PM) embedded with your business."
|
||||
},
|
||||
"consulting": {
|
||||
"title": "Technical Consulting",
|
||||
"description": "Architecture diagnostics, legacy modernization and technology roadmap definition."
|
||||
}
|
||||
}
|
||||
},
|
||||
"adaptivework": {
|
||||
"badge": "Planview Specialists",
|
||||
"title": "Planview AdaptiveWork support and evolution",
|
||||
"subtitle": "We help you get the most out of your portfolio management platform with implementation, customization, integrations and training.",
|
||||
"features": {
|
||||
"implementation": {
|
||||
"title": "Implementation & Setup",
|
||||
"description": "We configure workflows, hierarchies and templates tailored to your process."
|
||||
},
|
||||
"customization": {
|
||||
"title": "Advanced Customization",
|
||||
"description": "Calculated fields, automations, business rules and custom reports."
|
||||
},
|
||||
"integration": {
|
||||
"title": "Integrations",
|
||||
"description": "We connect AdaptiveWork with Jira, Azure DevOps, ERPs and internal tools."
|
||||
},
|
||||
"training": {
|
||||
"title": "Training & Support",
|
||||
"description": "Enablement for PMOs and end users plus continuous support from our team."
|
||||
}
|
||||
},
|
||||
"cta": "Tell me more"
|
||||
},
|
||||
"whyus": {
|
||||
"eyebrow": "Why RivusLab",
|
||||
"title": "A tech partner — not just a vendor",
|
||||
"items": {
|
||||
"senior": {
|
||||
"title": "Senior teams",
|
||||
"description": "Experienced engineers that drive technical decisions, not just lines of code."
|
||||
},
|
||||
"delivery": {
|
||||
"title": "Predictable delivery",
|
||||
"description": "Lean process, clear metrics and short validation cycles."
|
||||
},
|
||||
"communication": {
|
||||
"title": "Direct communication",
|
||||
"description": "No unnecessary layers: you talk to the people who actually build."
|
||||
},
|
||||
"quality": {
|
||||
"title": "Real quality",
|
||||
"description": "Testing, code review and observability as part of our daily routine."
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"eyebrow": "How we work",
|
||||
"title": "A simple, transparent and iterative process",
|
||||
"steps": {
|
||||
"diagnose": {
|
||||
"title": "Diagnose",
|
||||
"description": "We understand context, pain points and goals before any proposal."
|
||||
},
|
||||
"propose": {
|
||||
"title": "Propose",
|
||||
"description": "Scope, team, timeline and budget in a clear and objective proposal."
|
||||
},
|
||||
"execute": {
|
||||
"title": "Execute",
|
||||
"description": "Incremental delivery with agile rituals and full progress visibility."
|
||||
},
|
||||
"evolve": {
|
||||
"title": "Evolve",
|
||||
"description": "Post go-live support with maintenance and new features."
|
||||
}
|
||||
}
|
||||
},
|
||||
"cta": {
|
||||
"eyebrow": "Let's talk?",
|
||||
"title": "Share your challenge and we'll design the solution together",
|
||||
"subtitle": "We reply within 1 business day.",
|
||||
"emailLabel": "Email",
|
||||
"email": "contact@rivuslab.com",
|
||||
"whatsappLabel": "WhatsApp",
|
||||
"whatsapp": "+55 (00) 00000-0000",
|
||||
"ctaPrimary": "Send message"
|
||||
},
|
||||
"footer": {
|
||||
"tagline": "Software engineering & Planview AdaptiveWork specialists.",
|
||||
"navigation": "Navigation",
|
||||
"company": "Company",
|
||||
"contact": "Contact",
|
||||
"rights": "All rights reserved."
|
||||
},
|
||||
"lang": {
|
||||
"switchTo": "Change language"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"nav": {
|
||||
"services": "Servicios",
|
||||
"adaptivework": "AdaptiveWork",
|
||||
"process": "Proceso",
|
||||
"whyus": "Por qué nosotros",
|
||||
"contact": "Contacto",
|
||||
"cta": "Hablemos",
|
||||
"openMenu": "Abrir menú",
|
||||
"closeMenu": "Cerrar menú"
|
||||
},
|
||||
"theme": {
|
||||
"switchToLight": "Activar tema claro",
|
||||
"switchToDark": "Activar tema oscuro"
|
||||
},
|
||||
"hero": {
|
||||
"eyebrow": "Ingeniería de software & Planview AdaptiveWork",
|
||||
"title": "Ingeniería de software a la medida de tu negocio",
|
||||
"subtitle": "Somos una fábrica de software boutique. Entregamos equipos senior, código mantenible y soporte especializado en Planview AdaptiveWork para acelerar tus resultados.",
|
||||
"ctaPrimary": "Hablemos",
|
||||
"ctaSecondary": "Conoce nuestros servicios",
|
||||
"stats": {
|
||||
"experience": "años de experiencia",
|
||||
"projects": "proyectos entregados",
|
||||
"uptime": "satisfacción de clientes"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"eyebrow": "Servicios",
|
||||
"title": "Soluciones completas para el ciclo de vida de tu software",
|
||||
"subtitle": "Desde la concepción hasta la evolución continua, somos una extensión de tu equipo.",
|
||||
"items": {
|
||||
"outsourcing": {
|
||||
"title": "Outsourcing de Desarrollo",
|
||||
"description": "Desarrolladores y equipos completos bajo demanda, con gestión técnica y foco en la entrega."
|
||||
},
|
||||
"squads": {
|
||||
"title": "Squads Dedicados",
|
||||
"description": "Equipos multidisciplinarios (dev, QA, diseño, PM) trabajando junto a tu negocio."
|
||||
},
|
||||
"consulting": {
|
||||
"title": "Consultoría Técnica",
|
||||
"description": "Diagnóstico de arquitectura, modernización de legado y definición de roadmap tecnológico."
|
||||
}
|
||||
}
|
||||
},
|
||||
"adaptivework": {
|
||||
"badge": "Especialistas Planview",
|
||||
"title": "Soporte y evolución en Planview AdaptiveWork",
|
||||
"subtitle": "Sacamos el máximo provecho de tu plataforma de gestión de portafolio con implementación, personalización, integraciones y capacitación.",
|
||||
"features": {
|
||||
"implementation": {
|
||||
"title": "Implementación & Configuración",
|
||||
"description": "Configuramos flujos, jerarquías y plantillas adaptadas a tu proceso."
|
||||
},
|
||||
"customization": {
|
||||
"title": "Personalización Avanzada",
|
||||
"description": "Campos calculados, automatizaciones, reglas de negocio e informes a medida."
|
||||
},
|
||||
"integration": {
|
||||
"title": "Integraciones",
|
||||
"description": "Conectamos AdaptiveWork con Jira, Azure DevOps, ERPs y herramientas internas."
|
||||
},
|
||||
"training": {
|
||||
"title": "Capacitación & Soporte",
|
||||
"description": "Formación para PMOs y usuarios finales, además de soporte continuo de nuestro equipo."
|
||||
}
|
||||
},
|
||||
"cta": "Quiero saber más"
|
||||
},
|
||||
"whyus": {
|
||||
"eyebrow": "Por qué elegir RivusLab",
|
||||
"title": "Un socio tecnológico — no un proveedor más",
|
||||
"items": {
|
||||
"senior": {
|
||||
"title": "Equipos senior",
|
||||
"description": "Profesionales experimentados que aportan decisiones técnicas, no solo líneas de código."
|
||||
},
|
||||
"delivery": {
|
||||
"title": "Entrega predecible",
|
||||
"description": "Proceso ágil, métricas claras y ciclos cortos de validación."
|
||||
},
|
||||
"communication": {
|
||||
"title": "Comunicación directa",
|
||||
"description": "Sin capas innecesarias: hablas con quienes realmente construyen."
|
||||
},
|
||||
"quality": {
|
||||
"title": "Calidad real",
|
||||
"description": "Pruebas, code review y observabilidad como parte del día a día."
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"eyebrow": "Cómo trabajamos",
|
||||
"title": "Un proceso simple, transparente e iterativo",
|
||||
"steps": {
|
||||
"diagnose": {
|
||||
"title": "Diagnóstico",
|
||||
"description": "Entendemos el contexto, los dolores y los objetivos antes de cualquier propuesta."
|
||||
},
|
||||
"propose": {
|
||||
"title": "Propuesta",
|
||||
"description": "Alcance, equipo, plazo y presupuesto en una propuesta clara y objetiva."
|
||||
},
|
||||
"execute": {
|
||||
"title": "Ejecución",
|
||||
"description": "Entregas incrementales, con rituales ágiles y total visibilidad del progreso."
|
||||
},
|
||||
"evolve": {
|
||||
"title": "Evolución",
|
||||
"description": "Acompañamos el producto post go-live con mantenimiento y nuevas funcionalidades."
|
||||
}
|
||||
}
|
||||
},
|
||||
"cta": {
|
||||
"eyebrow": "¿Hablamos?",
|
||||
"title": "Cuéntanos tu desafío y diseñamos la solución contigo",
|
||||
"subtitle": "Respondemos en hasta 1 día hábil.",
|
||||
"emailLabel": "Correo",
|
||||
"email": "contacto@rivuslab.com",
|
||||
"whatsappLabel": "WhatsApp",
|
||||
"whatsapp": "+55 (00) 00000-0000",
|
||||
"ctaPrimary": "Enviar mensaje"
|
||||
},
|
||||
"footer": {
|
||||
"tagline": "Ingeniería de software & especialistas en Planview AdaptiveWork.",
|
||||
"navigation": "Navegación",
|
||||
"company": "Empresa",
|
||||
"contact": "Contacto",
|
||||
"rights": "Todos los derechos reservados."
|
||||
},
|
||||
"lang": {
|
||||
"switchTo": "Cambiar idioma"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"nav": {
|
||||
"services": "Serviços",
|
||||
"adaptivework": "AdaptiveWork",
|
||||
"process": "Processo",
|
||||
"whyus": "Por que nós",
|
||||
"contact": "Contato",
|
||||
"cta": "Fale com a gente",
|
||||
"openMenu": "Abrir menu",
|
||||
"closeMenu": "Fechar menu"
|
||||
},
|
||||
"theme": {
|
||||
"switchToLight": "Ativar tema claro",
|
||||
"switchToDark": "Ativar tema escuro"
|
||||
},
|
||||
"hero": {
|
||||
"eyebrow": "Engenharia de software & Planview AdaptiveWork",
|
||||
"title": "Engenharia de software sob medida para o seu negócio",
|
||||
"subtitle": "Somos uma fábrica de software boutique. Entregamos times sêniores, código sustentável e suporte especializado em Planview AdaptiveWork para acelerar seus resultados.",
|
||||
"ctaPrimary": "Fale com a gente",
|
||||
"ctaSecondary": "Conheça nossos serviços",
|
||||
"stats": {
|
||||
"experience": "anos de experiência",
|
||||
"projects": "projetos entregues",
|
||||
"uptime": "satisfação dos clientes"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"eyebrow": "Serviços",
|
||||
"title": "Soluções completas para o ciclo de vida do seu software",
|
||||
"subtitle": "Da concepção à evolução contínua, atuamos como extensão do seu time.",
|
||||
"items": {
|
||||
"outsourcing": {
|
||||
"title": "Outsourcing de Desenvolvimento",
|
||||
"description": "Desenvolvedores e times completos alocados sob demanda, com gestão técnica e foco em entrega."
|
||||
},
|
||||
"squads": {
|
||||
"title": "Squads Dedicadas",
|
||||
"description": "Times multidisciplinares (dev, QA, design, PM) trabalhando lado a lado com o seu negócio."
|
||||
},
|
||||
"consulting": {
|
||||
"title": "Consultoria Técnica",
|
||||
"description": "Diagnóstico de arquitetura, modernização de legado e definição de roadmap tecnológico."
|
||||
}
|
||||
}
|
||||
},
|
||||
"adaptivework": {
|
||||
"badge": "Especialistas Planview",
|
||||
"title": "Suporte e evolução em Planview AdaptiveWork",
|
||||
"subtitle": "Tiramos o máximo da sua plataforma de gestão de portfólio com implementação, customização, integrações e treinamento.",
|
||||
"features": {
|
||||
"implementation": {
|
||||
"title": "Implementação & Configuração",
|
||||
"description": "Configuramos workflows, hierarquias e templates aderentes ao seu processo."
|
||||
},
|
||||
"customization": {
|
||||
"title": "Customização Avançada",
|
||||
"description": "Campos calculados, automações, regras de negócio e relatórios sob medida."
|
||||
},
|
||||
"integration": {
|
||||
"title": "Integrações",
|
||||
"description": "Conectamos o AdaptiveWork com Jira, Azure DevOps, ERPs e ferramentas internas."
|
||||
},
|
||||
"training": {
|
||||
"title": "Treinamento & Suporte",
|
||||
"description": "Capacitação para PMOs e usuários finais e suporte contínuo da nossa equipe."
|
||||
}
|
||||
},
|
||||
"cta": "Quero saber mais"
|
||||
},
|
||||
"whyus": {
|
||||
"eyebrow": "Por que escolher a RivusLab",
|
||||
"title": "Parceria de tecnologia, não fornecedor",
|
||||
"items": {
|
||||
"senior": {
|
||||
"title": "Times sêniores",
|
||||
"description": "Profissionais experientes que entregam decisões técnicas, não só linhas de código."
|
||||
},
|
||||
"delivery": {
|
||||
"title": "Entrega previsível",
|
||||
"description": "Processo enxuto, métricas claras e ciclos curtos de validação."
|
||||
},
|
||||
"communication": {
|
||||
"title": "Comunicação direta",
|
||||
"description": "Sem camadas desnecessárias: você fala com quem realmente constrói."
|
||||
},
|
||||
"quality": {
|
||||
"title": "Qualidade de verdade",
|
||||
"description": "Testes, code review e observabilidade como parte do dia a dia."
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"eyebrow": "Como trabalhamos",
|
||||
"title": "Um processo simples, transparente e iterativo",
|
||||
"steps": {
|
||||
"diagnose": {
|
||||
"title": "Diagnóstico",
|
||||
"description": "Entendemos o contexto, dores e objetivos antes de qualquer proposta."
|
||||
},
|
||||
"propose": {
|
||||
"title": "Proposta",
|
||||
"description": "Escopo, time, prazo e orçamento em uma proposta clara e objetiva."
|
||||
},
|
||||
"execute": {
|
||||
"title": "Execução",
|
||||
"description": "Entregas incrementais, com rituais ágeis e visibilidade total do progresso."
|
||||
},
|
||||
"evolve": {
|
||||
"title": "Evolução",
|
||||
"description": "Acompanhamos o produto pós go-live com manutenção e novas features."
|
||||
}
|
||||
}
|
||||
},
|
||||
"cta": {
|
||||
"eyebrow": "Vamos conversar?",
|
||||
"title": "Conte seu desafio e desenhamos a solução com você",
|
||||
"subtitle": "Respondemos em até 1 dia útil.",
|
||||
"emailLabel": "E-mail",
|
||||
"email": "contato@rivuslab.com",
|
||||
"whatsappLabel": "WhatsApp",
|
||||
"whatsapp": "+55 (00) 00000-0000",
|
||||
"ctaPrimary": "Enviar mensagem"
|
||||
},
|
||||
"footer": {
|
||||
"tagline": "Engenharia de software & especialistas Planview AdaptiveWork.",
|
||||
"navigation": "Navegação",
|
||||
"company": "Empresa",
|
||||
"contact": "Contato",
|
||||
"rights": "Todos os direitos reservados."
|
||||
},
|
||||
"lang": {
|
||||
"switchTo": "Mudar idioma"
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
@import '@fontsource/inter/400.css';
|
||||
@import '@fontsource/inter/500.css';
|
||||
@import '@fontsource/inter/600.css';
|
||||
@import '@fontsource/inter/700.css';
|
||||
@import '@fontsource/inter/800.css';
|
||||
|
||||
@import './styles/theme.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-bg text-fg font-sans;
|
||||
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgb(var(--color-primary));
|
||||
color: rgb(var(--color-primary-fg));
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 2px solid rgb(var(--color-primary));
|
||||
outline-offset: 3px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.container-rl {
|
||||
@apply mx-auto w-full max-w-container px-gutter;
|
||||
}
|
||||
|
||||
.section {
|
||||
@apply py-section;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-md px-5 py-3
|
||||
font-semibold text-sm transition-all duration-200 ease-smooth
|
||||
focus-visible:outline-none disabled:opacity-50 disabled:pointer-events-none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply btn bg-primary text-primary-fg hover:brightness-95 hover:-translate-y-0.5
|
||||
shadow-sm hover:shadow-md;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply btn bg-secondary text-secondary-fg hover:brightness-110;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply btn bg-transparent text-fg hover:bg-bg-muted border border-border;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@apply inline-flex items-center gap-1.5 rounded-full bg-primary-soft
|
||||
px-3 py-1 text-xs font-semibold;
|
||||
color: rgb(var(--color-badge-fg) / 1);
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-xl border border-border bg-bg p-6 shadow-sm
|
||||
transition-all duration-300 ease-smooth;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@apply hover:-translate-y-1 hover:shadow-lg;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
border-color: rgb(var(--color-primary) / 0.4);
|
||||
}
|
||||
|
||||
.heading {
|
||||
@apply font-display font-bold tracking-tight text-fg;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
background-image:
|
||||
linear-gradient(to right, rgb(var(--color-border)) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgb(var(--color-border)) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
}
|
||||
|
||||
.bg-radial-fade {
|
||||
background: radial-gradient(
|
||||
ellipse at top,
|
||||
rgb(var(--color-primary-soft)) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Public assets in `public/` — filenames must match deployed files. */
|
||||
const LOGO_LIGHT = '/logo-light.png';
|
||||
const LOGO_DARK = '/logo-dark.png';
|
||||
|
||||
export function rivusLogoSrc(isDark: boolean): string {
|
||||
return isDark ? LOGO_DARK : LOGO_LIGHT;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export type SectionId =
|
||||
| 'services'
|
||||
| 'adaptivework'
|
||||
| 'process'
|
||||
| 'whyus'
|
||||
| 'contact';
|
||||
|
||||
export const NAV_SECTIONS: { id: SectionId; labelKey: `nav.${string}` }[] = [
|
||||
{ id: 'services', labelKey: 'nav.services' },
|
||||
{ id: 'adaptivework', labelKey: 'nav.adaptivework' },
|
||||
{ id: 'process', labelKey: 'nav.process' },
|
||||
{ id: 'whyus', labelKey: 'nav.whyus' },
|
||||
{ id: 'contact', labelKey: 'nav.contact' },
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
export const THEME_STORAGE_KEY = 'rivus-theme';
|
||||
|
||||
export type StoredTheme = 'light' | 'dark';
|
||||
|
||||
export function getStoredTheme(): StoredTheme | null {
|
||||
const raw = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (raw === 'light' || raw === 'dark') return raw;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveDark(stored: string | null, prefersDark: boolean): boolean {
|
||||
if (stored === 'dark') return true;
|
||||
if (stored === 'light') return false;
|
||||
return prefersDark;
|
||||
}
|
||||
|
||||
export function applyTheme(isDark: boolean): void {
|
||||
document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function readEffectiveDark(): boolean {
|
||||
return document.documentElement.dataset.theme === 'dark';
|
||||
}
|
||||
|
||||
export function toggleStoredTheme(): boolean {
|
||||
const nextDark = !readEffectiveDark();
|
||||
localStorage.setItem(THEME_STORAGE_KEY, nextDark ? 'dark' : 'light');
|
||||
applyTheme(nextDark);
|
||||
return nextDark;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import App from './App';
|
||||
import './i18n';
|
||||
import './index.css';
|
||||
|
||||
const root = document.getElementById('root');
|
||||
if (!root) throw new Error('Root element not found');
|
||||
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,82 @@
|
||||
/* =============================================================================
|
||||
THEME TOKENS — RivusLab
|
||||
-----------------------------------------------------------------------------
|
||||
This is the ONLY file you need to edit to change the brand identity.
|
||||
All tokens are exposed via CSS variables and consumed by Tailwind (see
|
||||
tailwind.config.ts) so changes here propagate to every component.
|
||||
|
||||
Colors are stored as "R G B" triplets so Tailwind can apply opacity
|
||||
modifiers (e.g. `bg-primary/40`).
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
/* ─── BRAND COLORS (R G B) ───────────────────────────────────────────────── */
|
||||
--color-primary: 35 196 192; /* #23C4C0 — teal */
|
||||
--color-primary-fg: 15 58 108; /* #0F3A6C — navy on primary */
|
||||
--color-primary-soft: 230 249 248; /* #E6F9F8 — tint of primary */
|
||||
|
||||
--color-secondary: 15 58 108; /* #0F3A6C — navy */
|
||||
--color-secondary-fg: 255 255 255;
|
||||
|
||||
--color-accent: 35 196 192;
|
||||
|
||||
/* ─── SURFACES ────────────────────────────────────────────────────────────── */
|
||||
--color-bg: 255 255 255;
|
||||
--color-bg-muted: 245 248 251; /* #F5F8FB */
|
||||
--color-bg-inverse: 15 58 108;
|
||||
|
||||
/* ─── TEXT ───────────────────────────────────────────────────────────────── */
|
||||
--color-fg: 15 27 45; /* #0F1B2D */
|
||||
--color-fg-muted: 90 107 130; /* #5A6B82 */
|
||||
--color-fg-inverse: 255 255 255;
|
||||
|
||||
/* ─── BORDERS ────────────────────────────────────────────────────────────── */
|
||||
--color-border: 227 233 241; /* #E3E9F1 */
|
||||
--color-border-strong: 201 212 226; /* #C9D4E2 */
|
||||
|
||||
/* ─── BADGE (pill labels on primary-soft) ─────────────────────────────────── */
|
||||
--color-badge-fg: 15 58 108; /* navy — readable on light primary-soft */
|
||||
|
||||
/* ─── TYPOGRAPHY ─────────────────────────────────────────────────────────── */
|
||||
--font-sans: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
--font-display: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
|
||||
/* ─── RADIUS ─────────────────────────────────────────────────────────────── */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 24px;
|
||||
--radius-2xl: 32px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* ─── SPACING / LAYOUT ───────────────────────────────────────────────────── */
|
||||
--space-section-y: 6rem; /* vertical padding for full sections */
|
||||
--space-container: 1.25rem; /* horizontal padding on small screens */
|
||||
--container-max: 1200px; /* max content width */
|
||||
|
||||
/* ─── SHADOWS ────────────────────────────────────────────────────────────── */
|
||||
--shadow-sm: 0 1px 2px rgb(15 27 45 / 0.06);
|
||||
--shadow-md: 0 6px 24px rgb(15 27 45 / 0.08);
|
||||
--shadow-lg: 0 20px 60px rgb(15 27 45 / 0.12);
|
||||
--shadow-glow: 0 0 0 6px rgb(35 196 192 / 0.18);
|
||||
}
|
||||
|
||||
/* ─── DARK THEME (optional) ──────────────────────────────────────────────────
|
||||
Activate by adding `data-theme="dark"` on <html>.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
[data-theme='dark'] {
|
||||
--color-bg: 11 22 38; /* #0B1626 */
|
||||
--color-bg-muted: 16 32 54; /* #102036 */
|
||||
--color-bg-inverse: 35 196 192;
|
||||
|
||||
--color-fg: 242 245 250; /* #F2F5FA */
|
||||
--color-fg-muted: 147 162 184; /* #93A2B8 */
|
||||
--color-fg-inverse: 15 27 45;
|
||||
|
||||
--color-border: 27 46 72; /* #1B2E48 */
|
||||
--color-border-strong: 42 67 102; /* #2A4366 */
|
||||
|
||||
--color-primary-soft: 18 50 78; /* tinted dark surface */
|
||||
|
||||
--color-badge-fg: 147 220 218; /* light teal — readable on dark primary-soft */
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user