translation added
This commit is contained in:
@@ -9,6 +9,7 @@ import DocIllustration from './DocIllustration';
|
||||
import Section from './Section';
|
||||
import Stat from './Stat';
|
||||
import StartButton from './StartButton';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface HeroProps {
|
||||
onStart: () => void;
|
||||
@@ -17,6 +18,23 @@ interface HeroProps {
|
||||
|
||||
const Hero: FC<HeroProps> = ({ onStart, blobY }) => {
|
||||
const isMobile = useIsMobile();
|
||||
const t = useTranslations('Hero');
|
||||
const tStats = useTranslations('Stats');
|
||||
|
||||
const getTranslatedLabel = (label: string) => {
|
||||
switch (label) {
|
||||
case 'Detection accuracy':
|
||||
return tStats('accuracy');
|
||||
case 'Documents checked':
|
||||
return tStats('documents');
|
||||
case 'Supported formats':
|
||||
return tStats('formats');
|
||||
case 'Report turnaround':
|
||||
return tStats('turnaround');
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', overflow: 'hidden', background: C.bg }}>
|
||||
@@ -71,7 +89,7 @@ const Hero: FC<HeroProps> = ({ onStart, blobY }) => {
|
||||
>
|
||||
{/* Text block */}
|
||||
<div style={{ maxWidth: 600, flex: 1 }}>
|
||||
<Badge>Academic Integrity Platform</Badge>
|
||||
<Badge>{t('badge')}</Badge>
|
||||
|
||||
<motion.div variants={stagger} initial="hidden" animate="visible">
|
||||
<motion.h1
|
||||
@@ -86,7 +104,7 @@ const Hero: FC<HeroProps> = ({ onStart, blobY }) => {
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Is Your Work
|
||||
{t('mainHeading')}
|
||||
</motion.h1>
|
||||
<motion.h1
|
||||
variants={fadeUp(0.1)}
|
||||
@@ -101,7 +119,7 @@ const Hero: FC<HeroProps> = ({ onStart, blobY }) => {
|
||||
marginBottom: 28,
|
||||
}}
|
||||
>
|
||||
Truly Original?
|
||||
{t('mainHeadingItalic')}
|
||||
</motion.h1>
|
||||
</motion.div>
|
||||
|
||||
@@ -117,10 +135,7 @@ const Hero: FC<HeroProps> = ({ onStart, blobY }) => {
|
||||
marginBottom: 40,
|
||||
}}
|
||||
>
|
||||
Plagiarism is presenting someone else‘s ideas or words as
|
||||
your own. In academia and professional life, it carries serious
|
||||
consequences. Our platform detects it in seconds — so you can
|
||||
submit with full confidence.
|
||||
{t('description')}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
@@ -142,7 +157,7 @@ const Hero: FC<HeroProps> = ({ onStart, blobY }) => {
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
}}
|
||||
>
|
||||
Certificate issued within 24h
|
||||
{t('certificateNote')}
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -165,9 +180,15 @@ const Hero: FC<HeroProps> = ({ onStart, blobY }) => {
|
||||
borderTop: `1px solid ${C.border}`,
|
||||
}}
|
||||
>
|
||||
{STATS.map((s) => (
|
||||
<Stat key={s.label} value={s.value} label={s.label} />
|
||||
))}
|
||||
{STATS.map((s) => {
|
||||
return (
|
||||
<Stat
|
||||
key={s.label}
|
||||
value={s.value}
|
||||
label={getTranslatedLabel(s.label)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -5,51 +5,82 @@ import { C } from '../tokens';
|
||||
import { INFO_CARDS } from '../constants';
|
||||
import InfoCard from './InfoCard';
|
||||
import Section from './Section';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const InfoSection: FC = () => (
|
||||
<div style={{ background: C.surfaceWarm }}>
|
||||
<Section style={{ paddingTop: 96, paddingBottom: 96 }}>
|
||||
{/* Heading */}
|
||||
<motion.div
|
||||
variants={fadeUp()}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
style={{ marginBottom: 48 }}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 11,
|
||||
color: C.accent,
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
Why It Matters
|
||||
</p>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: "'Playfair Display', serif",
|
||||
fontSize: 'clamp(26px, 3.5vw, 40px)',
|
||||
fontWeight: 700,
|
||||
color: C.text,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Understanding Plagiarism
|
||||
</h2>
|
||||
</motion.div>
|
||||
const InfoSection: FC = () => {
|
||||
const t = useTranslations('InfoSection');
|
||||
const tCards = useTranslations('InfoCards');
|
||||
|
||||
{/* Cards */}
|
||||
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
|
||||
{INFO_CARDS.map((card) => (
|
||||
<InfoCard key={card.title} {...card} />
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
const getTranslatedCard = (card: (typeof INFO_CARDS)[0]) => {
|
||||
switch (card.title) {
|
||||
case 'What is Plagiarism?':
|
||||
return {
|
||||
...card,
|
||||
title: tCards('card1Title'),
|
||||
text: tCards('card1Desc'),
|
||||
};
|
||||
case 'Why Check Your Document?':
|
||||
return {
|
||||
...card,
|
||||
title: tCards('card2Title'),
|
||||
text: tCards('card2Desc'),
|
||||
};
|
||||
case 'What You Get':
|
||||
return {
|
||||
...card,
|
||||
title: tCards('card3Title'),
|
||||
text: tCards('card3Desc'),
|
||||
};
|
||||
default:
|
||||
return card;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ background: C.surfaceWarm }}>
|
||||
<Section style={{ paddingTop: 96, paddingBottom: 96 }}>
|
||||
{/* Heading */}
|
||||
<motion.div
|
||||
variants={fadeUp()}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
style={{ marginBottom: 48 }}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 11,
|
||||
color: C.accent,
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{t('label')}
|
||||
</p>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: "'Playfair Display', serif",
|
||||
fontSize: 'clamp(26px, 3.5vw, 40px)',
|
||||
fontWeight: 700,
|
||||
color: C.text,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{t('heading')}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Cards */}
|
||||
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
|
||||
{INFO_CARDS.map((card) => (
|
||||
<InfoCard key={card.title} {...getTranslatedCard(card)} />
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoSection;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useState, type FC } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { C } from '../tokens';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface StartButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -10,6 +11,7 @@ interface StartButtonProps {
|
||||
|
||||
const StartButton: FC<StartButtonProps> = ({ onClick, small = false }) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const t = useTranslations('Common');
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
@@ -35,7 +37,7 @@ const StartButton: FC<StartButtonProps> = ({ onClick, small = false }) => {
|
||||
: `0 2px 8px ${C.accent}22`,
|
||||
}}
|
||||
>
|
||||
Start Checking →
|
||||
{t('startButton')}
|
||||
</motion.button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fadeUp } from '../animations';
|
||||
import { C } from '../tokens';
|
||||
import { STEPS } from '../constants';
|
||||
import type { Step } from '../types';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface StepCardProps {
|
||||
step: Step;
|
||||
@@ -14,6 +15,45 @@ const StepCard: FC<StepCardProps> = ({ step, index }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const inView = useInView(ref, { once: true, margin: '-50px' });
|
||||
const isLast = index === STEPS.length - 1;
|
||||
const t = useTranslations('Steps');
|
||||
|
||||
const getStepTitle = (num: string) => {
|
||||
switch (num) {
|
||||
case '01':
|
||||
return t('step1Title');
|
||||
case '02':
|
||||
return t('step2Title');
|
||||
case '03':
|
||||
return t('step3Title');
|
||||
case '04':
|
||||
return t('step4Title');
|
||||
case '05':
|
||||
return t('step5Title');
|
||||
case '06':
|
||||
return t('step6Title');
|
||||
default:
|
||||
return step.title;
|
||||
}
|
||||
};
|
||||
|
||||
const getStepDesc = (num: string) => {
|
||||
switch (num) {
|
||||
case '01':
|
||||
return t('step1Desc');
|
||||
case '02':
|
||||
return t('step2Desc');
|
||||
case '03':
|
||||
return t('step3Desc');
|
||||
case '04':
|
||||
return t('step4Desc');
|
||||
case '05':
|
||||
return t('step5Desc');
|
||||
case '06':
|
||||
return t('step6Desc');
|
||||
default:
|
||||
return step.desc;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -87,7 +127,7 @@ const StepCard: FC<StepCardProps> = ({ step, index }) => {
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{step.title}
|
||||
{getStepTitle(step.num)}
|
||||
</h3>
|
||||
</div>
|
||||
<p
|
||||
@@ -98,7 +138,7 @@ const StepCard: FC<StepCardProps> = ({ step, index }) => {
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{step.desc}
|
||||
{getStepDesc(step.num)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import Section from './Section';
|
||||
import StartButton from './StartButton';
|
||||
import StepCard from './StepCard';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface StepsSectionProps {
|
||||
stepsRef: React.RefObject<HTMLDivElement | null>;
|
||||
@@ -15,6 +16,7 @@ interface StepsSectionProps {
|
||||
|
||||
const StepsSection: FC<StepsSectionProps> = ({ stepsRef, onScrollTop }) => {
|
||||
const isMobile = useIsMobile();
|
||||
const t = useTranslations('StepsSection');
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -44,7 +46,7 @@ const StepsSection: FC<StepsSectionProps> = ({ stepsRef, onScrollTop }) => {
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
Process
|
||||
{t('label')}
|
||||
</p>
|
||||
<h2
|
||||
style={{
|
||||
@@ -55,7 +57,7 @@ const StepsSection: FC<StepsSectionProps> = ({ stepsRef, onScrollTop }) => {
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
How It Works
|
||||
{t('heading')}
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
@@ -65,7 +67,7 @@ const StepsSection: FC<StepsSectionProps> = ({ stepsRef, onScrollTop }) => {
|
||||
lineHeight: 1.75,
|
||||
}}
|
||||
>
|
||||
Six simple steps from upload to certified report.
|
||||
{t('description')}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
@@ -112,10 +114,10 @@ const StepsSection: FC<StepsSectionProps> = ({ stepsRef, onScrollTop }) => {
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Ready to verify your document?
|
||||
{t('ctaHeading')}
|
||||
</h3>
|
||||
<p style={{ color: C.textMuted, fontSize: 13 }}>
|
||||
Get your originality certificate in under 24 hours.
|
||||
{t('ctaDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<StartButton onClick={onScrollTop} />
|
||||
|
||||
@@ -2,10 +2,32 @@ import type { FC } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { C } from '../tokens';
|
||||
import { TICKER_ITEMS } from '../constants';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const Ticker: FC = () => {
|
||||
const t = useTranslations('Ticker');
|
||||
const doubled = [...TICKER_ITEMS, ...TICKER_ITEMS];
|
||||
|
||||
const getTranslatedItem = (item: string, index: number) => {
|
||||
const itemIndex = index % TICKER_ITEMS.length;
|
||||
switch (itemIndex) {
|
||||
case 0:
|
||||
return t('item1');
|
||||
case 1:
|
||||
return t('item2');
|
||||
case 2:
|
||||
return t('item3');
|
||||
case 3:
|
||||
return t('item4');
|
||||
case 4:
|
||||
return t('item5');
|
||||
case 5:
|
||||
return t('item6');
|
||||
default:
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -37,7 +59,7 @@ const Ticker: FC = () => {
|
||||
color: i % 3 === 0 ? C.textMid : C.textMuted,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
{getTranslatedItem(item, i)}
|
||||
<span style={{ marginLeft: 56, color: C.accent, opacity: 0.3 }}>
|
||||
✦
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { PaymentStatus } from '../lib/types';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
// ─── Payme Logo SVG ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -64,13 +65,14 @@ export const PaymeButton: React.FC<PaymeButtonProps> = ({
|
||||
status,
|
||||
}) => {
|
||||
const isLoading = status === 'loading';
|
||||
const t = useTranslations('Payment');
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={isLoading}
|
||||
aria-busy={isLoading}
|
||||
aria-label="Pay with Payme"
|
||||
aria-label={t('payButton')}
|
||||
className={`
|
||||
w-full flex items-center justify-center gap-3
|
||||
rounded-xl px-6 py-4
|
||||
@@ -87,7 +89,7 @@ export const PaymeButton: React.FC<PaymeButtonProps> = ({
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<span>Connecting to Payme…</span>
|
||||
<span>{t('connecting')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PaymentModalProps } from '../lib/types';
|
||||
import { getPricing } from '../lib/utils';
|
||||
import { PriceSummary } from './Pricesummary';
|
||||
import { PaymeButton } from './Paymebutton';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
// ─── Close Button ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -68,12 +69,14 @@ const CloseButton: React.FC<{ onClick: () => void }> = ({ onClick }) => (
|
||||
|
||||
// ─── Security Badge ────────────────────────────────────────────────────────────
|
||||
|
||||
const SecurityBadge: React.FC = () => (
|
||||
const SecurityBadge: React.FC<{ securityText: string }> = ({
|
||||
securityText,
|
||||
}) => (
|
||||
<div className="flex items-center justify-center gap-1.5 text-xs text-slate-400">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z" />
|
||||
</svg>
|
||||
<span>Secured by Payme · SSL encrypted</span>
|
||||
<span>{securityText}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -89,6 +92,7 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const pricing = getPricing();
|
||||
const status = isLoading ? 'loading' : 'idle';
|
||||
const t = useTranslations('Payment');
|
||||
|
||||
// ── Close on Escape ──────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
@@ -157,11 +161,9 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
|
||||
id="payment-modal-title"
|
||||
className="text-lg font-semibold text-slate-900"
|
||||
>
|
||||
Payment
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="mt-0.5 text-sm text-slate-500">
|
||||
Review your order and pay securely
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm text-slate-500">{t('description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -170,7 +172,7 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
|
||||
{/* Order details */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-widest text-slate-400 mb-3">
|
||||
Order Summary
|
||||
{t('orderSummary')}
|
||||
</h3>
|
||||
<PriceSummary hasCertificate={hasCertificate} pricing={pricing} />
|
||||
</div>
|
||||
@@ -187,20 +189,20 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
|
||||
>
|
||||
<path d="M12 1l2.753 5.527 6.247.907-4.5 4.385 1.063 6.181L12 15.027l-5.563 2.973 1.063-6.181L3 7.434l6.247-.907z" />
|
||||
</svg>
|
||||
<span>Certificate of completion included</span>
|
||||
<span>{t('certificateIncluded')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment method label */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-widest text-slate-400 mb-3">
|
||||
Payment Method
|
||||
{t('paymentMethod')}
|
||||
</h3>
|
||||
<PaymeButton onClick={onConfirmPayment} status={status} />
|
||||
</div>
|
||||
|
||||
{/* Security note */}
|
||||
<SecurityBadge />
|
||||
<SecurityBadge securityText={t('security')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from 'react';
|
||||
import { formatPrice } from '../lib/utils';
|
||||
import { PriceSummaryProps } from '../lib/types';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
// ─── Price Row ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -53,25 +54,26 @@ export const PriceSummary: React.FC<PriceSummaryProps> = ({
|
||||
const total = hasCertificate
|
||||
? pricing.serviceFee + pricing.certificateFee
|
||||
: pricing.serviceFee;
|
||||
const t = useTranslations('Payment');
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-slate-50 border border-slate-100 px-5 py-2 space-y-0">
|
||||
<PriceRow
|
||||
label="Service fee"
|
||||
label={t('serviceFee')}
|
||||
amount={pricing.serviceFee}
|
||||
currency={pricing.currency}
|
||||
/>
|
||||
|
||||
{hasCertificate && (
|
||||
<PriceRow
|
||||
label="Certificate"
|
||||
label={t('certificateLabel')}
|
||||
amount={pricing.certificateFee}
|
||||
currency={pricing.currency}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PriceRow
|
||||
label="Total"
|
||||
label={t('total')}
|
||||
amount={total}
|
||||
currency={pricing.currency}
|
||||
highlight
|
||||
|
||||
Reference in New Issue
Block a user