Livrare Lot 3 (Frontend): aplicație web, aplicație mobilă Android, extensie browser

- Surse complete web (React/Vite) + mobil (React Native/Expo) + extensie (MV3)
- Documentație de livrare: ghid utilizare, matrice trasabilitate cerințe, raport testare furnizor
- Artefacte binare: imagine Docker didi-frontend:lot3-1.0, APK, extensie v3.2.6 + SHA256SUMS
- Configurare adresă platformă externalizată (build args / .env / config.js)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Top Clossers 2026-07-17 12:33:59 +03:00
commit cec967f953
321 changed files with 80506 additions and 0 deletions

View file

@ -0,0 +1,116 @@
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import { useTranslation } from 'react-i18next';
import type { UsageStats } from '../../../services/subscription.service';
interface SubscriptionCardProps {
data: UsageStats;
onUpgrade: () => void;
}
const cardStyles = css`
background: var(--bg-surface); border: 1px solid var(--accent-border);
border-radius: 12px; padding: 24px; color: var(--fg-primary);
transition: all 0.3s ease;
box-sizing: border-box; min-width: 0; overflow: hidden;
&:hover { border-color: var(--accent); background: var(--bg-hover); }
@media (max-width: 480px) {
padding: 16px;
}
`;
const headerStyles = css`
display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; gap: 12px;
flex-wrap: wrap;
`;
const planNameStyles = css`
font-size: 28px; font-weight: bold; margin: 0 0 8px 0; color: var(--fg-primary);
word-break: break-word;
@media (max-width: 480px) {
font-size: 22px;
}
`;
const priceStyles = css`
font-size: 16px; color: var(--fg-secondary);
`;
const badgeStyles = css`
background: var(--accent-subtle); border: 1px solid var(--accent-border);
padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.5px; color: var(--fg-primary);
`;
const featureListStyles = css`list-style: none; padding: 0; margin: 20px 0;`;
const featureItemStyles = css`
display: flex; align-items: center; margin-bottom: 12px; font-size: 14px; color: var(--fg-primary);
&::before { content: '✓'; display: inline-block; width: 20px; height: 20px;
background: var(--accent); border-radius: 50%; margin-right: 12px;
text-align: center; line-height: 20px; font-weight: bold; color: var(--fg-on-accent); }
`;
const upgradeButtonStyles = css`
margin-top: 20px; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 600;
cursor: pointer; transition: all 0.3s ease;
background: var(--accent-subtle); border: 2px solid var(--accent); color: var(--fg-primary);
&:hover { transform: translateY(-2px); background: var(--accent-subtle);
border-color: var(--accent-hover); box-shadow: var(--shadow-md); }
`;
const periodInfoStyles = css`
font-size: 13px; color: var(--fg-secondary); margin-top: 16px; padding-top: 16px;
border-top: 1px solid var(--accent-border); word-break: break-word;
`;
export const SubscriptionCard = ({ data, onUpgrade }: SubscriptionCardProps) => {
const { t } = useTranslation();
const { plan, subscription, credits } = data;
const isFree = plan.priceAmount === 0;
const features = [
t('subscription.creditsPerCycleFeature', { count: plan.creditsPerCycle }),
t('subscription.storageGb', { count: plan.storageLimitGb }),
];
return (
<div css={cardStyles}>
<div css={headerStyles}>
<div>
<h2 css={planNameStyles}>{plan.name}</h2>
{plan.priceAmount > 0 && (
<div css={priceStyles}>${plan.priceAmount}/mo</div>
)}
</div>
<div css={badgeStyles}>
{subscription.isActive ? t('common.active') : t('common.inactive')}
</div>
</div>
<ul css={featureListStyles}>
{features.map((feature, i) => (
<li key={i} css={featureItemStyles}>{feature}</li>
))}
</ul>
{isFree && (
<button css={upgradeButtonStyles} onClick={onUpgrade}>
{t('subscription.upgradePlan')}
</button>
)}
<div css={periodInfoStyles}>
{isFree
? t('subscription.freePlanInfo')
: subscription.activationDate
? t('subscription.activeSince', { date: new Date(subscription.activationDate).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) })
: t('subscription.activeSubscription')
}
{credits.remaining > 0 && `${t('subscription.creditsRemaining', { count: credits.remaining })}`}
</div>
</div>
);
};