mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
fix: resolve React Compiler and linting errors
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m22s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m22s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
- Fix React Compiler memoization issues in useOrganizationPage.ts - Replace useCallback with useRef pattern in useKeyboard.ts - Remove unnecessary dependencies from useMemo hooks - Fix prettier formatting in api-client.ts and api-config.ts - Replace any types with proper types in error-handling, http-client, security - Remove unused imports and variables - Move ImpactBreakdownChart component outside render in ImpactMetrics.tsx - Fix setState in effect by using useMemo in HeritageBuildingPage.tsx - Memoize getHistoryTitle with useCallback in MatchDetailPage and MatchNegotiationPage - Add i18n for literal strings in community pages and LoginPage - Fix missing dependencies in DashboardPage and DiscoveryPage
This commit is contained in:
parent
bdb7673b16
commit
28f06d5787
@ -74,7 +74,7 @@ const Step1 = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
Upload additional images to showcase your organization (optional)
|
{t('organization.galleryImagesHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -128,10 +128,10 @@ export const ActivityFeed = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Activity</CardTitle>
|
<CardTitle>{t?.('activityFeed.recentActivity') || 'Recent Activity'}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{activities.map((activity) => (
|
{activities.map((activity) => (
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { clsx } from 'clsx';
|
|||||||
import { ResponsiveTable, Pagination, SearchBar, Checkbox, Button } from '@/components/ui';
|
import { ResponsiveTable, Pagination, SearchBar, Checkbox, Button } from '@/components/ui';
|
||||||
import { MoreVertical } from 'lucide-react';
|
import { MoreVertical } from 'lucide-react';
|
||||||
import { DropdownMenu } from '@/components/ui';
|
import { DropdownMenu } from '@/components/ui';
|
||||||
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
||||||
|
|
||||||
export interface DataTableColumn<T> {
|
export interface DataTableColumn<T> {
|
||||||
key: string;
|
key: string;
|
||||||
@ -90,6 +91,7 @@ export function DataTable<T>({
|
|||||||
renderMobileCard,
|
renderMobileCard,
|
||||||
className,
|
className,
|
||||||
}: DataTableProps<T>) {
|
}: DataTableProps<T>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const selectedCount = selection?.selectedRows.size || 0;
|
const selectedCount = selection?.selectedRows.size || 0;
|
||||||
const hasSelection = selectedCount > 0;
|
const hasSelection = selectedCount > 0;
|
||||||
|
|
||||||
@ -213,7 +215,7 @@ export function DataTable<T>({
|
|||||||
{/* Bulk Actions */}
|
{/* Bulk Actions */}
|
||||||
{hasSelection && bulkActions && bulkActions.length > 0 && (
|
{hasSelection && bulkActions && bulkActions.length > 0 && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted-foreground">{selectedCount} selected</span>
|
<span className="text-sm text-muted-foreground">{t('dataTable.selected', { count: selectedCount })}</span>
|
||||||
{bulkActions.map((action, index) => (
|
{bulkActions.map((action, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={index}
|
key={index}
|
||||||
@ -233,7 +235,7 @@ export function DataTable<T>({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => selection?.onSelectionChange(new Set())}
|
onClick={() => selection?.onSelectionChange(new Set())}
|
||||||
>
|
>
|
||||||
Clear
|
{t('dataTable.clear')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -251,7 +253,7 @@ export function DataTable<T>({
|
|||||||
}}
|
}}
|
||||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-muted-foreground">Select all ({data.length} items)</span>
|
<span className="text-sm text-muted-foreground">{t('dataTable.selectAll', { count: data.length })}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -290,7 +292,7 @@ export function DataTable<T>({
|
|||||||
/>
|
/>
|
||||||
{pagination.onPageSizeChange && (
|
{pagination.onPageSizeChange && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted-foreground">Items per page:</span>
|
<span className="text-sm text-muted-foreground">{t('dataTable.itemsPerPage')}</span>
|
||||||
<select
|
<select
|
||||||
value={pagination.pageSize}
|
value={pagination.pageSize}
|
||||||
onChange={(e) => pagination.onPageSizeChange?.(Number(e.target.value))}
|
onChange={(e) => pagination.onPageSizeChange?.(Number(e.target.value))}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { Filter, X } from 'lucide-react';
|
import { Filter, X } from 'lucide-react';
|
||||||
import { Button, Popover, SelectDropdown, CheckboxGroup } from '@/components/ui';
|
import { Button, Popover, SelectDropdown, CheckboxGroup } from '@/components/ui';
|
||||||
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
||||||
|
|
||||||
export interface FilterOption {
|
export interface FilterOption {
|
||||||
id: string;
|
id: string;
|
||||||
@ -27,6 +28,7 @@ export interface FilterBarProps {
|
|||||||
* Advanced filter bar component
|
* Advanced filter bar component
|
||||||
*/
|
*/
|
||||||
export const FilterBar = ({ filters, values, onChange, onReset, className }: FilterBarProps) => {
|
export const FilterBar = ({ filters, values, onChange, onReset, className }: FilterBarProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const activeFilterCount = Object.values(values).filter(
|
const activeFilterCount = Object.values(values).filter(
|
||||||
(v) => v !== null && v !== undefined && (Array.isArray(v) ? v.length > 0 : true)
|
(v) => v !== null && v !== undefined && (Array.isArray(v) ? v.length > 0 : true)
|
||||||
@ -59,7 +61,7 @@ export const FilterBar = ({ filters, values, onChange, onReset, className }: Fil
|
|||||||
trigger={
|
trigger={
|
||||||
<Button variant={hasActiveFilters ? 'primary' : 'outline'} size="sm" className="relative">
|
<Button variant={hasActiveFilters ? 'primary' : 'outline'} size="sm" className="relative">
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
<Filter className="h-4 w-4 mr-2" />
|
||||||
Filters
|
{t('filterBar.filters')}
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<span className="ml-2 rounded-full bg-primary-foreground px-1.5 py-0.5 text-xs text-primary">
|
<span className="ml-2 rounded-full bg-primary-foreground px-1.5 py-0.5 text-xs text-primary">
|
||||||
{activeFilterCount}
|
{activeFilterCount}
|
||||||
@ -70,11 +72,11 @@ export const FilterBar = ({ filters, values, onChange, onReset, className }: Fil
|
|||||||
content={
|
content={
|
||||||
<div className="w-80 p-4 space-y-4">
|
<div className="w-80 p-4 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="font-semibold">Filters</h3>
|
<h3 className="font-semibold">{t('filterBar.filters')}</h3>
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<Button variant="ghost" size="sm" onClick={handleReset}>
|
<Button variant="ghost" size="sm" onClick={handleReset}>
|
||||||
<X className="h-4 w-4 mr-1" />
|
<X className="h-4 w-4 mr-1" />
|
||||||
Clear All
|
{t('filterBar.clearAll')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -157,10 +159,10 @@ export const FilterBar = ({ filters, values, onChange, onReset, className }: Fil
|
|||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||||
<Button variant="outline" size="sm" onClick={() => setIsOpen(false)}>
|
<Button variant="outline" size="sm" onClick={() => setIsOpen(false)}>
|
||||||
Cancel
|
{t('filterBar.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="sm" onClick={() => setIsOpen(false)}>
|
<Button variant="primary" size="sm" onClick={() => setIsOpen(false)}>
|
||||||
Apply Filters
|
{t('filterBar.applyFilters')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Avatar, DropdownMenu } from '@/components/ui';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useMaintenanceSetting } from '@/hooks/api/useAdminAPI';
|
import { useMaintenanceSetting } from '@/hooks/api/useAdminAPI';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Bell,
|
Bell,
|
||||||
@ -116,6 +117,7 @@ const defaultNavItems: AdminNavItem[] = [
|
|||||||
* Admin Layout with sidebar navigation
|
* Admin Layout with sidebar navigation
|
||||||
*/
|
*/
|
||||||
export const AdminLayout = ({ children, title, breadcrumbs }: AdminLayoutProps) => {
|
export const AdminLayout = ({ children, title, breadcrumbs }: AdminLayoutProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -178,7 +180,7 @@ export const AdminLayout = ({ children, title, breadcrumbs }: AdminLayoutProps)
|
|||||||
<div className="flex h-16 items-center justify-between border-b px-4">
|
<div className="flex h-16 items-center justify-between border-b px-4">
|
||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-lg font-semibold">Admin Panel</h1>
|
<h1 className="text-lg font-semibold">{t('adminPanel.title')}</h1>
|
||||||
<button
|
<button
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className="rounded-md p-1 hover:bg-muted"
|
className="rounded-md p-1 hover:bg-muted"
|
||||||
@ -334,7 +336,7 @@ export const AdminLayout = ({ children, title, breadcrumbs }: AdminLayoutProps)
|
|||||||
{/* Show maintenance banner for admins */}
|
{/* Show maintenance banner for admins */}
|
||||||
{maintenance.data?.enabled && (
|
{maintenance.data?.enabled && (
|
||||||
<div className="mb-4 p-3 bg-yellow-50 border-l-4 border-yellow-400">
|
<div className="mb-4 p-3 bg-yellow-50 border-l-4 border-yellow-400">
|
||||||
<strong className="block">Maintenance mode is active</strong>
|
<strong className="block">{t('adminPanel.maintenanceModeActive')}</strong>
|
||||||
<div className="text-sm">{maintenance.data?.message}</div>
|
<div className="text-sm">{maintenance.data?.message}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { usePermissions } from '@/hooks/usePermissions';
|
import { usePermissions } from '@/hooks/usePermissions';
|
||||||
import { Permission } from '@/types/permissions';
|
import { Permission } from '@/types/permissions';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
||||||
|
|
||||||
export interface PermissionGateProps {
|
export interface PermissionGateProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -21,6 +22,7 @@ export const PermissionGate = ({
|
|||||||
fallback = null,
|
fallback = null,
|
||||||
showError = false,
|
showError = false,
|
||||||
}: PermissionGateProps) => {
|
}: PermissionGateProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { checkAnyPermission, checkAllPermissions } = usePermissions();
|
const { checkAnyPermission, checkAllPermissions } = usePermissions();
|
||||||
|
|
||||||
const permissions = Array.isArray(permission) ? permission : [permission];
|
const permissions = Array.isArray(permission) ? permission : [permission];
|
||||||
@ -30,7 +32,7 @@ export const PermissionGate = ({
|
|||||||
if (showError) {
|
if (showError) {
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-destructive">
|
<div className="text-sm text-destructive">
|
||||||
You don't have permission to view this content.
|
{t('permissionGate.noPermission')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const ImpactMetricsSection = ({ totalCo2Saved, totalEconomicValue, activeMatches
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Heading level="h3" className="text-green-600 mb-1">
|
<Heading level="h3" className="text-green-600 mb-1">
|
||||||
{totalCo2Saved} t
|
{totalCo2Saved} {t('dashboard.co2Unit')}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text variant="muted" className="text-xs">
|
<Text variant="muted" className="text-xs">
|
||||||
{t('dashboard.perYear')}
|
{t('dashboard.perYear')}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFieldArray, Control, FieldErrors, FieldValues, Path } from 'react-hook-form';
|
import { useFieldArray, Control, FieldErrors, FieldValues, Path } from 'react-hook-form';
|
||||||
import Button from '@/components/ui/Button.tsx';
|
import Button from '@/components/ui/Button.tsx';
|
||||||
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
||||||
|
|
||||||
interface DynamicFieldArrayProps<T extends FieldValues> {
|
interface DynamicFieldArrayProps<T extends FieldValues> {
|
||||||
control: Control<T>;
|
control: Control<T>;
|
||||||
@ -21,6 +22,7 @@ const DynamicFieldArray = <T extends FieldValues>({
|
|||||||
defaultItem,
|
defaultItem,
|
||||||
children,
|
children,
|
||||||
}: DynamicFieldArrayProps<T>) => {
|
}: DynamicFieldArrayProps<T>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { fields, append, remove } = useFieldArray({ control, name });
|
const { fields, append, remove } = useFieldArray({ control, name });
|
||||||
const arrayErrors = errors[name as string];
|
const arrayErrors = errors[name as string];
|
||||||
|
|
||||||
@ -36,6 +38,7 @@ const DynamicFieldArray = <T extends FieldValues>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
className="h-10 w-10 p-0 shrink-0 mt-1.5"
|
className="h-10 w-10 p-0 shrink-0 mt-1.5"
|
||||||
|
aria-label={t('form.removeItem')}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Heading, Text } from '@/components/ui/Typography.tsx';
|
|||||||
import { getIconByName } from '@/lib/heritage-mapper.tsx';
|
import { getIconByName } from '@/lib/heritage-mapper.tsx';
|
||||||
import type { HeritageSource, TimelineItem } from '@/types';
|
import type { HeritageSource, TimelineItem } from '@/types';
|
||||||
import { motion, Variants } from 'framer-motion';
|
import { motion, Variants } from 'framer-motion';
|
||||||
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
@ -117,6 +118,7 @@ interface TimelineItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TimelineItem: React.FC<TimelineItemProps> = ({ item, index, sources }) => {
|
const TimelineItem: React.FC<TimelineItemProps> = ({ item, index, sources }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
// Debug: log when items render to help diagnose missing chronology
|
// Debug: log when items render to help diagnose missing chronology
|
||||||
|
|
||||||
console.debug('[TimelineItem] render', { id: item.id, title: item.title, index });
|
console.debug('[TimelineItem] render', { id: item.id, title: item.title, index });
|
||||||
@ -192,7 +194,7 @@ const TimelineItem: React.FC<TimelineItemProps> = ({ item, index, sources }) =>
|
|||||||
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
<div className="absolute bottom-4 right-4 flex items-center gap-2 text-white text-sm">
|
<div className="absolute bottom-4 right-4 flex items-center gap-2 text-white text-sm">
|
||||||
<ZoomIn className="w-4 h-4" />
|
<ZoomIn className="w-4 h-4" />
|
||||||
<span className="font-medium">View</span>
|
<span className="font-medium">{t('heritage.view')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -88,10 +88,10 @@ const TimelineSection = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Filter className="w-5 h-5 text-primary" />
|
<Filter className="w-5 h-5 text-primary" />
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Filters
|
{t('heritage.filters')}
|
||||||
{(filters.selectedCategory !== 'all' || filters.minImportance > 1) && (
|
{(filters.selectedCategory !== 'all' || filters.minImportance > 1) && (
|
||||||
<span className="ml-2 text-sm text-muted-foreground">
|
<span className="ml-2 text-sm text-muted-foreground">
|
||||||
({timelineItems.length} events)
|
{t('heritage.eventsCount', { count: timelineItems.length })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@ -99,6 +99,7 @@ const TimelineSection = ({
|
|||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: filters.showFilters ? 180 : 0 }}
|
animate={{ rotate: filters.showFilters ? 180 : 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
|
aria-label={t('heritage.toggleFilters')}
|
||||||
>
|
>
|
||||||
▼
|
▼
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -114,7 +115,7 @@ const TimelineSection = ({
|
|||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{/* Category Filter */}
|
{/* Category Filter */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium mb-2 block">Category</label>
|
<label className="text-sm font-medium mb-2 block">{t('heritage.category')}</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => filters.setSelectedCategory('all')}
|
onClick={() => filters.setSelectedCategory('all')}
|
||||||
@ -124,7 +125,7 @@ const TimelineSection = ({
|
|||||||
: 'bg-muted hover:bg-muted/80'
|
: 'bg-muted hover:bg-muted/80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All
|
{t('heritage.all')}
|
||||||
</button>
|
</button>
|
||||||
{availableCategories.map((category) => (
|
{availableCategories.map((category) => (
|
||||||
<button
|
<button
|
||||||
@ -145,7 +146,7 @@ const TimelineSection = ({
|
|||||||
{/* Importance Filter */}
|
{/* Importance Filter */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium mb-2 block">
|
<label className="text-sm font-medium mb-2 block">
|
||||||
Minimum Importance: {filters.minImportance}
|
{t('heritage.minimumImportance', { value: filters.minImportance })}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@ -167,7 +168,7 @@ const TimelineSection = ({
|
|||||||
onClick={filters.resetFilters}
|
onClick={filters.resetFilters}
|
||||||
className="w-full px-4 py-2 text-sm bg-muted hover:bg-muted/80 rounded-lg transition-colors"
|
className="w-full px-4 py-2 text-sm bg-muted hover:bg-muted/80 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Reset Filters
|
{t('heritage.resetFilters')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -184,7 +185,7 @@ const TimelineSection = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-center py-16">
|
<div className="text-center py-16">
|
||||||
<p className="text-muted-foreground text-lg">
|
<p className="text-muted-foreground text-lg">
|
||||||
No events match your filters. Try adjusting your selection.
|
{t('heritage.noEventsMatch')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -295,8 +295,8 @@ const ResourceExchangeVisualization: React.FC<ResourceExchangeVisualizationProps
|
|||||||
<div className="w-full flex flex-col items-center gap-4 max-w-lg mx-auto">
|
<div className="w-full flex flex-col items-center gap-4 max-w-lg mx-auto">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-sm font-semibold text-foreground mb-1">Resource Exchange Network</h3>
|
<h3 className="text-sm font-semibold text-foreground mb-1">{t('resourceExchange.networkTitle')}</h3>
|
||||||
<p className="text-xs text-muted-foreground">Businesses connect to exchange resources</p>
|
<p className="text-xs text-muted-foreground">{t('resourceExchange.networkDescription')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SVG Canvas for network visualization */}
|
{/* SVG Canvas for network visualization */}
|
||||||
@ -648,7 +648,7 @@ const ResourceExchangeVisualization: React.FC<ResourceExchangeVisualizationProps
|
|||||||
>
|
>
|
||||||
{sectorConnections.length}
|
{sectorConnections.length}
|
||||||
</motion.text>
|
</motion.text>
|
||||||
<title>{sectorConnections.length} resource exchange connections</title>
|
<title>{t('resourceExchange.connectionsCount', { count: sectorConnections.length })}</title>
|
||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
@ -754,7 +754,7 @@ const ResourceExchangeVisualization: React.FC<ResourceExchangeVisualizationProps
|
|||||||
|
|
||||||
{/* Legend - moved below the animation */}
|
{/* Legend - moved below the animation */}
|
||||||
<div className="flex gap-4 items-center bg-background/80 backdrop-blur-sm px-4 py-2 rounded-full border shadow-lg">
|
<div className="flex gap-4 items-center bg-background/80 backdrop-blur-sm px-4 py-2 rounded-full border shadow-lg">
|
||||||
<span className="text-xs text-muted-foreground font-medium">Resource Exchanges:</span>
|
<span className="text-xs text-muted-foreground font-medium">{t('resourceExchange.resourceExchanges')}</span>
|
||||||
{RESOURCE_TYPES.map((resource) => {
|
{RESOURCE_TYPES.map((resource) => {
|
||||||
const Icon = resource.icon;
|
const Icon = resource.icon;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -64,13 +64,13 @@ const HistoricalSidebarPreview = () => {
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-base font-semibold mb-3">Местоположение и статус</h4>
|
<h4 className="text-base font-semibold mb-3">{t('mapSidebar.locationAndStatus')}</h4>
|
||||||
<dl>
|
<dl>
|
||||||
<InfoLine label="Адрес" value={landmark.address} />
|
<InfoLine label="Адрес" value={landmark.address} />
|
||||||
<InfoLine label="Текущий статус" value={landmark.currentStatus} />
|
<InfoLine label="Текущий статус" value={landmark.currentStatus} />
|
||||||
{relatedOrg && (
|
{relatedOrg && (
|
||||||
<>
|
<>
|
||||||
<dt className="text-xs text-muted-foreground">Связанная организация</dt>
|
<dt className="text-xs text-muted-foreground">{t('mapSidebar.relatedOrganization')}</dt>
|
||||||
<dd className="text-sm font-medium">
|
<dd className="text-sm font-medium">
|
||||||
{relatedOrg.Name}
|
{relatedOrg.Name}
|
||||||
<Button
|
<Button
|
||||||
@ -90,7 +90,7 @@ const HistoricalSidebarPreview = () => {
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-base font-semibold mb-3">Исторический контекст</h4>
|
<h4 className="text-base font-semibold mb-3">{t('mapSidebar.historicalContext')}</h4>
|
||||||
<dl>
|
<dl>
|
||||||
<InfoLine label="Основатель/Владелец" value={landmark.builder} />
|
<InfoLine label="Основатель/Владелец" value={landmark.builder} />
|
||||||
<InfoLine label="Архитектор" value={landmark.architect} />
|
<InfoLine label="Архитектор" value={landmark.architect} />
|
||||||
|
|||||||
@ -103,7 +103,7 @@ const MatchLine = React.memo<{
|
|||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{t('matchesMap.distance', 'Distance')}:
|
{t('matchesMap.distance', 'Distance')}:
|
||||||
</span>
|
</span>
|
||||||
<div className="font-medium">{match.DistanceKm.toFixed(1)} km</div>
|
<div className="font-medium">{t('matchesMap.distanceValue', { distance: match.DistanceKm.toFixed(1) })}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Package, Wrench } from 'lucide-react';
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { renderToStaticMarkup } from 'react-dom/server';
|
import { renderToStaticMarkup } from 'react-dom/server';
|
||||||
import { Marker, Popup } from 'react-leaflet';
|
import { Marker, Popup } from 'react-leaflet';
|
||||||
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
||||||
|
|
||||||
interface ProductServiceMarkersProps {
|
interface ProductServiceMarkersProps {
|
||||||
showProducts: boolean;
|
showProducts: boolean;
|
||||||
@ -19,6 +20,7 @@ const ProductMarker = React.memo<{
|
|||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: (match: DiscoveryMatch) => void;
|
onSelect: (match: DiscoveryMatch) => void;
|
||||||
}>(({ match, isSelected, onSelect }) => {
|
}>(({ match, isSelected, onSelect }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const position: LatLngTuple = useMemo(() => {
|
const position: LatLngTuple = useMemo(() => {
|
||||||
if (!match.product?.location) return [0, 0];
|
if (!match.product?.location) return [0, 0];
|
||||||
return [match.product.location.latitude, match.product.location.longitude];
|
return [match.product.location.latitude, match.product.location.longitude];
|
||||||
@ -89,7 +91,7 @@ const ProductMarker = React.memo<{
|
|||||||
<div className="flex items-center gap-4 text-sm">
|
<div className="flex items-center gap-4 text-sm">
|
||||||
<span className="font-medium">€{match.product.unit_price.toFixed(2)}</span>
|
<span className="font-medium">€{match.product.unit_price.toFixed(2)}</span>
|
||||||
{match.product.moq > 0 && (
|
{match.product.moq > 0 && (
|
||||||
<span className="text-muted-foreground">MOQ: {match.product.moq}</span>
|
<span className="text-muted-foreground">{t('productService.moq', { value: match.product.moq })}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{match.organization && (
|
{match.organization && (
|
||||||
|
|||||||
@ -20,12 +20,12 @@ export const useOrganizationPage = (organization: Organization | undefined) => {
|
|||||||
const handleAnalyzeSymbiosis = useCallback(() => {
|
const handleAnalyzeSymbiosis = useCallback(() => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
ai.handleAnalyzeSymbiosis();
|
ai.handleAnalyzeSymbiosis();
|
||||||
}, [isAuthenticated, ai.handleAnalyzeSymbiosis]);
|
}, [isAuthenticated, ai]);
|
||||||
|
|
||||||
const handleFetchWebIntelligence = useCallback(() => {
|
const handleFetchWebIntelligence = useCallback(() => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
ai.handleFetchWebIntelligence();
|
ai.handleFetchWebIntelligence();
|
||||||
}, [isAuthenticated, ai.handleFetchWebIntelligence]);
|
}, [isAuthenticated, ai]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
symbiosisState: ai.symbiosisState,
|
symbiosisState: ai.symbiosisState,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
interface UseKeyboardOptions {
|
interface UseKeyboardOptions {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@ -13,15 +13,23 @@ export function useKeyboard(
|
|||||||
options: UseKeyboardOptions = {}
|
options: UseKeyboardOptions = {}
|
||||||
) {
|
) {
|
||||||
const { enabled = true, target = document } = options;
|
const { enabled = true, target = document } = options;
|
||||||
|
const handlerRef = useRef(handler);
|
||||||
|
|
||||||
const memoizedHandler = useCallback(handler, [handler]);
|
// Update handler ref when handler changes
|
||||||
|
useEffect(() => {
|
||||||
|
handlerRef.current = handler;
|
||||||
|
}, [handler]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
|
|
||||||
target.addEventListener('keydown', memoizedHandler);
|
const wrappedHandler = (event: KeyboardEvent) => {
|
||||||
return () => target.removeEventListener('keydown', memoizedHandler);
|
handlerRef.current(event);
|
||||||
}, [enabled, target, memoizedHandler]);
|
};
|
||||||
|
|
||||||
|
target.addEventListener('keydown', wrappedHandler);
|
||||||
|
return () => target.removeEventListener('keydown', wrappedHandler);
|
||||||
|
}, [enabled, target]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -166,7 +166,7 @@ export const useOrganizationFilter = (
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [organizations, debouncedSearchTerm, selectedSectors, sortOption, t]);
|
}, [organizations, debouncedSearchTerm, selectedSectors, sortOption]);
|
||||||
|
|
||||||
return filteredAndSortedOrgs;
|
return filteredAndSortedOrgs;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -89,10 +89,7 @@ function parseApiResponse<T>(response: Response, schema: z.ZodSchema<T>): Promis
|
|||||||
/**
|
/**
|
||||||
* Schema-validated API GET request
|
* Schema-validated API GET request
|
||||||
*/
|
*/
|
||||||
export async function apiGetValidated<T>(
|
export async function apiGetValidated<T>(endpoint: string, schema: z.ZodSchema<T>): Promise<T> {
|
||||||
endpoint: string,
|
|
||||||
schema: z.ZodSchema<T>
|
|
||||||
): Promise<T> {
|
|
||||||
const data = await apiGet<unknown>(endpoint);
|
const data = await apiGet<unknown>(endpoint);
|
||||||
const result = schema.safeParse(data);
|
const result = schema.safeParse(data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
* Centralizes API endpoint configuration for consistent routing across all services
|
* Centralizes API endpoint configuration for consistent routing across all services
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
// API Version Configuration
|
// API Version Configuration
|
||||||
export const API_CONFIG = {
|
export const API_CONFIG = {
|
||||||
VERSION: 'v1',
|
VERSION: 'v1',
|
||||||
|
|||||||
@ -350,17 +350,20 @@ export class ErrorHandler {
|
|||||||
*/
|
*/
|
||||||
private sendToSentry(error: AppError): void {
|
private sendToSentry(error: AppError): void {
|
||||||
// Placeholder for Sentry integration
|
// Placeholder for Sentry integration
|
||||||
if (typeof window !== 'undefined' && (window as any).Sentry) {
|
if (typeof window !== 'undefined') {
|
||||||
(window as any).Sentry.captureException(error.originalError || new Error(error.message), {
|
const sentry = (window as Window & { Sentry?: { captureException: (error: Error, context?: Record<string, unknown>) => void } }).Sentry;
|
||||||
tags: {
|
if (sentry) {
|
||||||
category: error.category,
|
sentry.captureException(error.originalError || new Error(error.message), {
|
||||||
severity: error.severity,
|
tags: {
|
||||||
},
|
category: error.category,
|
||||||
extra: {
|
severity: error.severity,
|
||||||
errorId: error.id,
|
},
|
||||||
context: error.context,
|
extra: {
|
||||||
},
|
errorId: error.id,
|
||||||
});
|
context: error.context,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,11 +372,14 @@ export class ErrorHandler {
|
|||||||
*/
|
*/
|
||||||
private sendToAnalytics(error: AppError): void {
|
private sendToAnalytics(error: AppError): void {
|
||||||
// Placeholder for analytics integration
|
// Placeholder for analytics integration
|
||||||
if (typeof window !== 'undefined' && (window as any).gtag) {
|
if (typeof window !== 'undefined') {
|
||||||
(window as any).gtag('event', 'exception', {
|
const gtag = (window as Window & { gtag?: (event: string, name: string, params: Record<string, unknown>) => void }).gtag;
|
||||||
description: error.message,
|
if (gtag) {
|
||||||
fatal: error.severity === ErrorSeverity.CRITICAL,
|
gtag('event', 'exception', {
|
||||||
});
|
description: error.message,
|
||||||
|
fatal: error.severity === ErrorSeverity.CRITICAL,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -401,7 +401,7 @@ export const httpClient = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorData: any;
|
let errorData: { error?: string } | string;
|
||||||
try {
|
try {
|
||||||
errorData = await response.json();
|
errorData = await response.json();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export class InputSanitizer {
|
|||||||
export class SecurityMonitor {
|
export class SecurityMonitor {
|
||||||
private static violations: SecurityViolation[] = [];
|
private static violations: SecurityViolation[] = [];
|
||||||
|
|
||||||
static logViolation(type: string, details: Record<string, any>): void {
|
static logViolation(type: string, details: Record<string, unknown>): void {
|
||||||
const violation: SecurityViolation = {
|
const violation: SecurityViolation = {
|
||||||
type,
|
type,
|
||||||
details,
|
details,
|
||||||
@ -182,7 +182,7 @@ export class SecurityMonitor {
|
|||||||
|
|
||||||
interface SecurityViolation {
|
interface SecurityViolation {
|
||||||
type: string;
|
type: string;
|
||||||
details: Record<string, any>;
|
details: Record<string, unknown>;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* Provides enhanced type safety with runtime validation and better generic constraints
|
* Provides enhanced type safety with runtime validation and better generic constraints
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z, ZodSchema, ZodTypeAny } from 'zod';
|
import { z, ZodSchema } from 'zod';
|
||||||
|
|
||||||
// Type-safe API response wrapper
|
// Type-safe API response wrapper
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export const en = {
|
|||||||
organization: {
|
organization: {
|
||||||
logo: 'Logo',
|
logo: 'Logo',
|
||||||
galleryImages: 'Gallery Images',
|
galleryImages: 'Gallery Images',
|
||||||
|
galleryImagesHint: 'Upload additional images to showcase your organization (optional)',
|
||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
kicker: 'Open Beta',
|
kicker: 'Open Beta',
|
||||||
@ -202,6 +203,28 @@ export const en = {
|
|||||||
activityFeed: {
|
activityFeed: {
|
||||||
recentActivity: 'Recent Activity',
|
recentActivity: 'Recent Activity',
|
||||||
},
|
},
|
||||||
|
dataTable: {
|
||||||
|
selected: '{{count}} selected',
|
||||||
|
clear: 'Clear',
|
||||||
|
selectAll: 'Select all ({{count}} items)',
|
||||||
|
itemsPerPage: 'Items per page:',
|
||||||
|
},
|
||||||
|
filterBar: {
|
||||||
|
filters: 'Filters',
|
||||||
|
clearAll: 'Clear All',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
applyFilters: 'Apply Filters',
|
||||||
|
},
|
||||||
|
adminPanel: {
|
||||||
|
title: 'Admin Panel',
|
||||||
|
maintenanceModeActive: 'Maintenance mode is active',
|
||||||
|
},
|
||||||
|
permissionGate: {
|
||||||
|
noPermission: "You don't have permission to view this content.",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
removeItem: 'Remove item',
|
||||||
|
},
|
||||||
time: {
|
time: {
|
||||||
justNow: 'just now',
|
justNow: 'just now',
|
||||||
minutesAgo_one: '{{count}} minute ago',
|
minutesAgo_one: '{{count}} minute ago',
|
||||||
@ -343,6 +366,9 @@ export const en = {
|
|||||||
fetchWebIntelButton: 'Get Summary',
|
fetchWebIntelButton: 'Get Summary',
|
||||||
webIntelSources: 'Sources:',
|
webIntelSources: 'Sources:',
|
||||||
viewOrganization: 'Go to organization',
|
viewOrganization: 'Go to organization',
|
||||||
|
locationAndStatus: 'Местоположение и статус',
|
||||||
|
relatedOrganization: 'Связанная организация',
|
||||||
|
historicalContext: 'Исторический контекст',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
chatbot: {
|
chatbot: {
|
||||||
@ -802,6 +828,15 @@ export const en = {
|
|||||||
floorArea: 'Floor Area',
|
floorArea: 'Floor Area',
|
||||||
location: 'Location',
|
location: 'Location',
|
||||||
coordinates: 'Coordinates',
|
coordinates: 'Coordinates',
|
||||||
|
view: 'View',
|
||||||
|
filters: 'Filters',
|
||||||
|
eventsCount: '({{count}} events)',
|
||||||
|
toggleFilters: 'Toggle filters',
|
||||||
|
category: 'Category',
|
||||||
|
all: 'All',
|
||||||
|
minimumImportance: 'Minimum Importance: {{value}}',
|
||||||
|
resetFilters: 'Reset Filters',
|
||||||
|
noEventsMatch: 'No events match your filters. Try adjusting your selection.',
|
||||||
sources: 'Sources & References',
|
sources: 'Sources & References',
|
||||||
},
|
},
|
||||||
similarOrganizations: {
|
similarOrganizations: {
|
||||||
@ -1094,6 +1129,7 @@ export const en = {
|
|||||||
noMatches: 'No matches found',
|
noMatches: 'No matches found',
|
||||||
adjustFilters: 'Try adjusting your filters',
|
adjustFilters: 'Try adjusting your filters',
|
||||||
hideFilters: 'Hide',
|
hideFilters: 'Hide',
|
||||||
|
distanceValue: '{{distance}} km',
|
||||||
showFilters: 'Filters',
|
showFilters: 'Filters',
|
||||||
matchId: 'Match ID',
|
matchId: 'Match ID',
|
||||||
compatibility: 'Compatibility',
|
compatibility: 'Compatibility',
|
||||||
@ -1153,6 +1189,7 @@ export const en = {
|
|||||||
resourceFlows: 'Resource Flows',
|
resourceFlows: 'Resource Flows',
|
||||||
matches: 'Matches',
|
matches: 'Matches',
|
||||||
co2Saved: 'CO₂ Saved',
|
co2Saved: 'CO₂ Saved',
|
||||||
|
co2Unit: 't',
|
||||||
perYear: 'per year',
|
perYear: 'per year',
|
||||||
economicValue: 'Economic Value',
|
economicValue: 'Economic Value',
|
||||||
created: 'created annually',
|
created: 'created annually',
|
||||||
@ -1493,4 +1530,13 @@ export const en = {
|
|||||||
lastYear: 'Last Year',
|
lastYear: 'Last Year',
|
||||||
allTime: 'All Time',
|
allTime: 'All Time',
|
||||||
},
|
},
|
||||||
|
resourceExchange: {
|
||||||
|
networkTitle: 'Resource Exchange Network',
|
||||||
|
networkDescription: 'Businesses connect to exchange resources',
|
||||||
|
connectionsCount: '{{count}} resource exchange connections',
|
||||||
|
resourceExchanges: 'Resource Exchanges:',
|
||||||
|
},
|
||||||
|
productService: {
|
||||||
|
moq: 'MOQ: {{value}}',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
|
import { useTranslation } from '@/hooks/useI18n';
|
||||||
|
|
||||||
const CommunityEventsPage = () => {
|
const CommunityEventsPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-3xl font-bold mb-6">Community Events</h1>
|
<h1 className="text-3xl font-bold mb-6">{t('community.events.title')}</h1>
|
||||||
<div className="bg-muted p-8 rounded-lg text-center">
|
<div className="bg-muted p-8 rounded-lg text-center">
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
Community events calendar coming soon! Find sustainability workshops, networking events,
|
{t('community.events.description')}
|
||||||
and environmental awareness campaigns.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
|
import { useTranslation } from '@/hooks/useI18n';
|
||||||
|
|
||||||
const CommunityImpactPage = () => {
|
const CommunityImpactPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-3xl font-bold mb-6">Community Impact Dashboard</h1>
|
<h1 className="text-3xl font-bold mb-6">{t('community.impact.title')}</h1>
|
||||||
<div className="bg-muted p-8 rounded-lg text-center">
|
<div className="bg-muted p-8 rounded-lg text-center">
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
Community impact dashboard coming soon! Track environmental and economic benefits of
|
{t('community.impact.description')}
|
||||||
industrial symbiosis.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
|
import { useTranslation } from '@/hooks/useI18n';
|
||||||
|
|
||||||
const CommunityNewsPage = () => {
|
const CommunityNewsPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-3xl font-bold mb-6">Community News</h1>
|
<h1 className="text-3xl font-bold mb-6">{t('community.news.title')}</h1>
|
||||||
<div className="bg-muted p-8 rounded-lg text-center">
|
<div className="bg-muted p-8 rounded-lg text-center">
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
Community news feature coming soon! Stay tuned for local sustainability news and updates.
|
{t('community.news.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
|
import { useTranslation } from '@/hooks/useI18n';
|
||||||
|
|
||||||
const CommunityStoriesPage = () => {
|
const CommunityStoriesPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-3xl font-bold mb-6">Community Stories</h1>
|
<h1 className="text-3xl font-bold mb-6">{t('community.stories.title')}</h1>
|
||||||
<div className="bg-muted p-8 rounded-lg text-center">
|
<div className="bg-muted p-8 rounded-lg text-center">
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
Success stories and case studies coming soon! Read about successful resource connections
|
{t('community.stories.description')}
|
||||||
and sustainability achievements.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -118,6 +118,7 @@ const DashboardPage = () => {
|
|||||||
proposalsData,
|
proposalsData,
|
||||||
userOrganizations,
|
userOrganizations,
|
||||||
pendingProposals,
|
pendingProposals,
|
||||||
|
proposals.length,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Filter activities based on selected filter
|
// Filter activities based on selected filter
|
||||||
@ -144,7 +145,6 @@ const DashboardPage = () => {
|
|||||||
// Number formatting handled by shared utilities inside components
|
// Number formatting handled by shared utilities inside components
|
||||||
|
|
||||||
const isLoading = isLoadingDashboard || isLoadingPlatform || isLoadingMatching || isLoadingImpact;
|
const isLoading = isLoadingDashboard || isLoadingPlatform || isLoadingMatching || isLoadingImpact;
|
||||||
const hasError = dashboardError || platformError || matchingError || impactError;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
|
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { Heading, Text } from '@/components/ui/Typography';
|
|||||||
import { useTranslation } from '@/hooks/useI18n';
|
import { useTranslation } from '@/hooks/useI18n';
|
||||||
import { useNavigation } from '@/hooks/useNavigation';
|
import { useNavigation } from '@/hooks/useNavigation';
|
||||||
import { universalSearch, type DiscoveryMatch, type SearchQuery } from '@/services/discovery-api';
|
import { universalSearch, type DiscoveryMatch, type SearchQuery } from '@/services/discovery-api';
|
||||||
import { MapPin, Package, Users, Wrench } from 'lucide-react';
|
import { Package, Users, Wrench } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
@ -92,6 +92,7 @@ export default function DiscoveryPage() {
|
|||||||
// This is not a "search" so hasActiveSearch will be false
|
// This is not a "search" so hasActiveSearch will be false
|
||||||
handleSearch({ limit: 20 });
|
handleSearch({ limit: 20 });
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderMatchCard = (match: DiscoveryMatch) => {
|
const renderMatchCard = (match: DiscoveryMatch) => {
|
||||||
|
|||||||
@ -12,11 +12,10 @@ import {
|
|||||||
Ruler,
|
Ruler,
|
||||||
User,
|
User,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { MainLayout } from '@/components/layout/MainLayout.tsx';
|
import { MainLayout } from '@/components/layout/MainLayout.tsx';
|
||||||
import Badge from '@/components/ui/Badge.tsx';
|
import Badge from '@/components/ui/Badge.tsx';
|
||||||
import Spinner from '@/components/ui/Spinner';
|
|
||||||
import { Heading, Text } from '@/components/ui/Typography.tsx';
|
import { Heading, Text } from '@/components/ui/Typography.tsx';
|
||||||
import { LoadingState } from '@/components/ui/LoadingState.tsx';
|
import { LoadingState } from '@/components/ui/LoadingState.tsx';
|
||||||
import { useHeritageSites } from '@/hooks/api/useHeritageSitesAPI';
|
import { useHeritageSites } from '@/hooks/api/useHeritageSitesAPI';
|
||||||
@ -28,13 +27,12 @@ const HeritageBuildingPage = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: heritageSites, isLoading } = useHeritageSites();
|
const { data: heritageSites, isLoading } = useHeritageSites();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [building, setBuilding] = useState<BackendHeritageSite | null>(null);
|
|
||||||
|
const building = useMemo(() => {
|
||||||
useEffect(() => {
|
|
||||||
if (heritageSites && id) {
|
if (heritageSites && id) {
|
||||||
const foundBuilding = heritageSites.find((site) => String(site.ID) === id);
|
return heritageSites.find((site) => String(site.ID) === id) || null;
|
||||||
setBuilding(foundBuilding || null);
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}, [heritageSites, id]);
|
}, [heritageSites, id]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -338,7 +336,7 @@ const HeritageBuildingPage = () => {
|
|||||||
{t('heritage.floorArea') || 'Floor Area'}
|
{t('heritage.floorArea') || 'Floor Area'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="muted" className="text-sm">
|
<Text variant="muted" className="text-sm">
|
||||||
{building.FloorAreaM2} m²
|
{t('heritage.floorAreaValue', { value: building.FloorAreaM2 })}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,6 +11,57 @@ import { Award, Briefcase, DollarSign, Globe, Target, TrendingUp, Zap } from 'lu
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { formatCurrency, formatNumber } from '../lib/fin';
|
import { formatCurrency, formatNumber } from '../lib/fin';
|
||||||
|
|
||||||
|
// Types for impact metrics data
|
||||||
|
interface TopImpactingMatch {
|
||||||
|
id?: string;
|
||||||
|
description?: string;
|
||||||
|
resource_type?: string;
|
||||||
|
co2_impact?: number;
|
||||||
|
economic_impact?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface YearlyProjection {
|
||||||
|
co2_projected?: number;
|
||||||
|
economic_projected?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple visualization component for impact breakdown
|
||||||
|
const ImpactBreakdownChart = ({
|
||||||
|
data,
|
||||||
|
title,
|
||||||
|
color = 'hsl(var(--primary))',
|
||||||
|
}: {
|
||||||
|
data: Record<string, number>;
|
||||||
|
title: string;
|
||||||
|
color?: string;
|
||||||
|
}) => {
|
||||||
|
const entries = Object.entries(data);
|
||||||
|
const maxValue = Math.max(...entries.map(([, value]) => value));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-medium text-muted-foreground">{title}</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{entries.map(([key, value]) => (
|
||||||
|
<div key={key} className="flex items-center gap-3">
|
||||||
|
<span className="text-sm min-w-20 truncate capitalize">{key.replace('_', ' ')}</span>
|
||||||
|
<div className="flex-1 bg-muted rounded-full h-3">
|
||||||
|
<div
|
||||||
|
className="h-3 rounded-full transition-all duration-500"
|
||||||
|
style={{
|
||||||
|
width: `${(value / maxValue) * 100}%`,
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium min-w-16 text-right">{formatNumber(value)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ImpactMetrics = () => {
|
const ImpactMetrics = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
|
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
|
||||||
@ -54,45 +105,6 @@ const ImpactMetrics = () => {
|
|||||||
};
|
};
|
||||||
}, [impactMetrics, platformStats]);
|
}, [impactMetrics, platformStats]);
|
||||||
|
|
||||||
// using central fin utilities
|
|
||||||
|
|
||||||
// Simple visualization component for impact breakdown
|
|
||||||
const ImpactBreakdownChart = ({
|
|
||||||
data,
|
|
||||||
title,
|
|
||||||
color = 'hsl(var(--primary))',
|
|
||||||
}: {
|
|
||||||
data: Record<string, number>;
|
|
||||||
title: string;
|
|
||||||
color?: string;
|
|
||||||
}) => {
|
|
||||||
const entries = Object.entries(data);
|
|
||||||
const maxValue = Math.max(...entries.map(([_, value]) => value));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="text-sm font-medium text-muted-foreground">{title}</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{entries.map(([key, value]) => (
|
|
||||||
<div key={key} className="flex items-center gap-3">
|
|
||||||
<span className="text-sm min-w-20 truncate capitalize">{key.replace('_', ' ')}</span>
|
|
||||||
<div className="flex-1 bg-muted rounded-full h-3">
|
|
||||||
<div
|
|
||||||
className="h-3 rounded-full transition-all duration-500"
|
|
||||||
style={{
|
|
||||||
width: `${(value / maxValue) * 100}%`,
|
|
||||||
backgroundColor: color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium min-w-16 text-right">{formatNumber(value)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Impact category icons
|
// Impact category icons
|
||||||
const getCategoryIcon = (category: string) => {
|
const getCategoryIcon = (category: string) => {
|
||||||
switch (category.toLowerCase()) {
|
switch (category.toLowerCase()) {
|
||||||
@ -315,7 +327,7 @@ const ImpactMetrics = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{impact.topImpactingMatches.slice(0, 5).map((match: any, index: number) => (
|
{impact.topImpactingMatches.slice(0, 5).map((match: TopImpactingMatch, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center justify-between p-4 border rounded-lg"
|
className="flex items-center justify-between p-4 border rounded-lg"
|
||||||
@ -325,13 +337,13 @@ const ImpactMetrics = () => {
|
|||||||
{index + 1}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{match.description || `Match ${match.id}`}</p>
|
<p className="font-medium">{match.description || `Match ${match.id || index}`}</p>
|
||||||
<p className="text-sm text-muted-foreground">{match.resource_type}</p>
|
<p className="text-sm text-muted-foreground">{match.resource_type}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="font-semibold text-green-600">
|
<p className="font-semibold text-green-600">
|
||||||
{formatNumber(match.co2_impact || 0)} t CO₂
|
{t('impactMetrics.co2Tonnes', { value: formatNumber(match.co2_impact || 0) })}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{formatCurrency(match.economic_impact || 0)}
|
{formatCurrency(match.economic_impact || 0)}
|
||||||
@ -382,7 +394,7 @@ const ImpactMetrics = () => {
|
|||||||
{Object.keys(impact.yearlyProjections).length > 0 ? (
|
{Object.keys(impact.yearlyProjections).length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Object.entries(impact.yearlyProjections).map(
|
{Object.entries(impact.yearlyProjections).map(
|
||||||
([year, projection]: [string, any]) => (
|
([year, projection]: [string, YearlyProjection]) => (
|
||||||
<div
|
<div
|
||||||
key={year}
|
key={year}
|
||||||
className="flex justify-between items-center p-3 bg-muted/50 rounded"
|
className="flex justify-between items-center p-3 bg-muted/50 rounded"
|
||||||
@ -390,7 +402,7 @@ const ImpactMetrics = () => {
|
|||||||
<span className="font-medium">{year}</span>
|
<span className="font-medium">{year}</span>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="font-semibold text-green-600">
|
<div className="font-semibold text-green-600">
|
||||||
{formatNumber(projection.co2_projected || 0)} t CO₂
|
{t('impactMetrics.co2Tonnes', { value: formatNumber(projection.co2_projected || 0) })}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{formatCurrency(projection.economic_projected || 0)}
|
{formatCurrency(projection.economic_projected || 0)}
|
||||||
@ -455,8 +467,7 @@ const ImpactMetrics = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-purple-700">
|
<p className="text-sm text-purple-700">
|
||||||
{t('impactMetrics.activeConnections')}
|
{t('impactMetrics.activeConnections', { count: impact.activeMatchesCount })}
|
||||||
.replace('{{ count }}', impact.activeMatchesCount.toString())
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@ -105,7 +105,7 @@ const LoginPage = () => {
|
|||||||
{isDevelopment && (
|
{isDevelopment && (
|
||||||
<div className="mb-6 p-4 bg-muted rounded-lg border border-primary/20">
|
<div className="mb-6 p-4 bg-muted rounded-lg border border-primary/20">
|
||||||
<p className="text-sm font-medium mb-3 text-foreground">
|
<p className="text-sm font-medium mb-3 text-foreground">
|
||||||
Quick Login (Development)
|
{t('login.quickLogin')}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{TEST_USERS.map((testUser) => (
|
{TEST_USERS.map((testUser) => (
|
||||||
@ -129,7 +129,7 @@ const LoginPage = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 pt-3 border-t border-border">
|
<div className="mt-3 pt-3 border-t border-border">
|
||||||
<p className="text-xs text-muted-foreground mb-2">Test Credentials:</p>
|
<p className="text-xs text-muted-foreground mb-2">{t('login.testCredentials')}</p>
|
||||||
<div className="space-y-1 text-xs font-mono">
|
<div className="space-y-1 text-xs font-mono">
|
||||||
{TEST_USERS.map((user) => (
|
{TEST_USERS.map((user) => (
|
||||||
<div key={user.email} className="flex items-center gap-2">
|
<div key={user.email} className="flex items-center gap-2">
|
||||||
|
|||||||
@ -24,13 +24,12 @@ import {
|
|||||||
MapPin,
|
MapPin,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { formatCurrency } from '../lib/fin';
|
import { formatCurrency } from '../lib/fin';
|
||||||
|
|
||||||
const MatchDetailPage = () => {
|
const MatchDetailPage = () => {
|
||||||
const { id: matchId } = useParams<{ id: string }>();
|
const { id: matchId } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
|
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@ -42,6 +41,19 @@ const MatchDetailPage = () => {
|
|||||||
const [newStatus, setNewStatus] = useState('');
|
const [newStatus, setNewStatus] = useState('');
|
||||||
const [statusNotes, setStatusNotes] = useState('');
|
const [statusNotes, setStatusNotes] = useState('');
|
||||||
|
|
||||||
|
const getHistoryTitle = useCallback((action: string, value?: string) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'status_change':
|
||||||
|
return t('matchDetail.statusChanged');
|
||||||
|
case 'comment':
|
||||||
|
return t('matchDetail.commentAdded');
|
||||||
|
case 'update':
|
||||||
|
return t('matchDetail.matchUpdated');
|
||||||
|
default:
|
||||||
|
return value || action;
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
// Transform match history to timeline format
|
// Transform match history to timeline format
|
||||||
const timelineEntries: TimelineEntry[] = useMemo(() => {
|
const timelineEntries: TimelineEntry[] = useMemo(() => {
|
||||||
if (!match?.History) return [];
|
if (!match?.History) return [];
|
||||||
@ -56,20 +68,7 @@ const MatchDetailPage = () => {
|
|||||||
oldValue: entry.old_value,
|
oldValue: entry.old_value,
|
||||||
newValue: entry.new_value,
|
newValue: entry.new_value,
|
||||||
}));
|
}));
|
||||||
}, [match?.History]);
|
}, [match?.History, getHistoryTitle]);
|
||||||
|
|
||||||
const getHistoryTitle = (action: string, value?: string) => {
|
|
||||||
switch (action) {
|
|
||||||
case 'status_change':
|
|
||||||
return t('matchDetail.statusChanged');
|
|
||||||
case 'comment':
|
|
||||||
return t('matchDetail.commentAdded');
|
|
||||||
case 'update':
|
|
||||||
return t('matchDetail.matchUpdated');
|
|
||||||
default:
|
|
||||||
return value || action;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusUpdate = async () => {
|
const handleStatusUpdate = async () => {
|
||||||
if (!match || !newStatus || !user) return;
|
if (!match || !newStatus || !user) return;
|
||||||
@ -229,7 +228,7 @@ const MatchDetailPage = () => {
|
|||||||
{t('matchDetail.paybackPeriod')}
|
{t('matchDetail.paybackPeriod')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-semibold">
|
<span className="text-sm font-semibold">
|
||||||
{match.EconomicImpact.payback_years.toFixed(1)} years
|
{t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1) })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { formatCurrency } from '../lib/fin';
|
import { formatCurrency } from '../lib/fin';
|
||||||
|
|
||||||
@ -46,6 +46,19 @@ const MatchNegotiationPage = () => {
|
|||||||
const [showMessageModal, setShowMessageModal] = useState(false);
|
const [showMessageModal, setShowMessageModal] = useState(false);
|
||||||
const [messageText, setMessageText] = useState('');
|
const [messageText, setMessageText] = useState('');
|
||||||
|
|
||||||
|
const getHistoryTitle = useCallback((action: string, value?: string) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'status_changed':
|
||||||
|
return t('matchNegotiation.statusChanged');
|
||||||
|
case 'comment':
|
||||||
|
return t('matchNegotiation.commentAdded');
|
||||||
|
case 'update':
|
||||||
|
return t('matchNegotiation.matchUpdated');
|
||||||
|
default:
|
||||||
|
return value || action;
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
// Transform match history to timeline format
|
// Transform match history to timeline format
|
||||||
const timelineEntries: TimelineEntry[] = useMemo(() => {
|
const timelineEntries: TimelineEntry[] = useMemo(() => {
|
||||||
if (!match?.History) return [];
|
if (!match?.History) return [];
|
||||||
@ -60,20 +73,7 @@ const MatchNegotiationPage = () => {
|
|||||||
oldValue: entry.old_value,
|
oldValue: entry.old_value,
|
||||||
newValue: entry.new_value,
|
newValue: entry.new_value,
|
||||||
}));
|
}));
|
||||||
}, [match?.History]);
|
}, [match?.History, getHistoryTitle]);
|
||||||
|
|
||||||
const getHistoryTitle = (action: string, value?: string) => {
|
|
||||||
switch (action) {
|
|
||||||
case 'status_changed':
|
|
||||||
return t('matchNegotiation.statusChanged');
|
|
||||||
case 'comment':
|
|
||||||
return t('matchNegotiation.commentAdded');
|
|
||||||
case 'update':
|
|
||||||
return t('matchNegotiation.matchUpdated');
|
|
||||||
default:
|
|
||||||
return value || action;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get allowed next statuses based on current status
|
// Get allowed next statuses based on current status
|
||||||
const allowedNextStatuses = useMemo(() => {
|
const allowedNextStatuses = useMemo(() => {
|
||||||
@ -325,7 +325,7 @@ const MatchNegotiationPage = () => {
|
|||||||
{t('matchNegotiation.co2Avoided')}
|
{t('matchNegotiation.co2Avoided')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-semibold text-success">
|
<span className="text-sm font-semibold text-success">
|
||||||
{match.EconomicImpact.co2_avoided_tonnes.toFixed(1)} t/year
|
{t('matchNegotiation.co2TonnesPerYear', { value: match.EconomicImpact.co2_avoided_tonnes.toFixed(1) })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -335,7 +335,7 @@ const MatchNegotiationPage = () => {
|
|||||||
{t('matchDetail.paybackPeriod')}
|
{t('matchDetail.paybackPeriod')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-semibold">
|
<span className="text-sm font-semibold">
|
||||||
{match.EconomicImpact.payback_years.toFixed(1)} years
|
{t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1) })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user