Import source: app/src/widgets/CompanyWidget
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
// Embeddable company profile card.
|
||||||
|
// Fetches from /api/companies/:slug; falls back to TeknavData static seed.
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { TeknavData } from '../../teknav-data.js';
|
||||||
|
|
||||||
|
const SITE_URL = 'https://www.teknav.ir';
|
||||||
|
|
||||||
|
const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
const cardStyle = {
|
||||||
|
fontFamily: '\'Vazirmatn\', Tahoma, sans-serif',
|
||||||
|
direction: 'rtl',
|
||||||
|
maxWidth: 480,
|
||||||
|
border: isDark ? '1px solid rgba(255,255,255,0.1)' : '1px solid #E4DDD2',
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: isDark ? '#0C1E22' : '#fff',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyWidget({ slug }) {
|
||||||
|
const [company, setCompany] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const lastHeight = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`/api/companies/${encodeURIComponent(slug)}`)
|
||||||
|
.then((r) => r.ok ? r.json() : null)
|
||||||
|
.then((data) => {
|
||||||
|
if (data) { setCompany(data); }
|
||||||
|
else {
|
||||||
|
// Static fallback from seed data
|
||||||
|
const fallback = (TeknavData.companies ?? []).find((c) => c.slug === slug);
|
||||||
|
if (fallback) setCompany(fallback);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
const fallback = (TeknavData.companies ?? []).find((c) => c.slug === slug);
|
||||||
|
if (fallback) setCompany(fallback);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = document.getElementById('widget-root');
|
||||||
|
if (!el) return;
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const h = entry.target.offsetHeight || 0;
|
||||||
|
if (h !== lastHeight.current) {
|
||||||
|
lastHeight.current = h;
|
||||||
|
window.parent?.postMessage({ type: 'resize', height: h }, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ ...cardStyle, padding: 24, display: 'flex', gap: 12, alignItems: 'center' }}>
|
||||||
|
<div style={{ width: 40, height: 40, borderRadius: 8, background: isDark ? 'rgba(255,255,255,0.08)' : '#E4DDD2', flexShrink: 0 }} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ height: 14, borderRadius: 4, background: isDark ? 'rgba(255,255,255,0.08)' : '#E4DDD2', width: '60%', marginBottom: 8 }} />
|
||||||
|
<div style={{ height: 10, borderRadius: 4, background: isDark ? 'rgba(255,255,255,0.05)' : '#F4EFE6', width: '40%' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!company) {
|
||||||
|
return <div style={{ ...cardStyle, padding: 20, color: isDark ? 'rgba(255,255,255,0.5)' : '#5F6B6D', fontSize: 14 }}>شرکت یافت نشد.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={cardStyle}>
|
||||||
|
<div style={{ background: isDark ? '#061014' : '#0C1E22', padding: '16px 20px', display: 'flex', gap: 12, alignItems: 'center' }}>
|
||||||
|
{company.logoUrl && (
|
||||||
|
<img src={company.logoUrl} alt={company.name} style={{ width: 40, height: 40, borderRadius: 8, objectFit: 'contain', background: '#fff', padding: 4 }} />
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ color: '#fff', fontWeight: 700, fontSize: 16 }}>{company.name}</div>
|
||||||
|
{company.verified && (
|
||||||
|
<span style={{ color: '#D49A2A', fontSize: 11, fontWeight: 600 }}>✓ تأیید شده</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{company.category && (
|
||||||
|
<span style={{ background: 'rgba(212,154,42,0.15)', color: '#D49A2A', fontSize: 11, padding: '2px 8px', borderRadius: 20 }}>
|
||||||
|
{company.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '16px 20px' }}>
|
||||||
|
{company.description && (
|
||||||
|
<p style={{ fontSize: 14, lineHeight: 1.9, margin: '0 0 12px', color: isDark ? 'rgba(255,255,255,0.8)' : '#263238' }}>{company.description}</p>
|
||||||
|
)}
|
||||||
|
{(company.techStack || []).length > 0 && (
|
||||||
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||||
|
{company.techStack.map((tech) => (
|
||||||
|
<span key={tech} style={{
|
||||||
|
background: isDark ? 'rgba(255,255,255,0.08)' : '#E4DDD2',
|
||||||
|
color: isDark ? 'rgba(255,255,255,0.7)' : '#263238',
|
||||||
|
fontSize: 11, padding: '3px 10px', borderRadius: 20,
|
||||||
|
}}>{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
borderTop: isDark ? '1px solid rgba(255,255,255,0.08)' : '1px solid #E4DDD2',
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: isDark ? '#061014' : '#F8F5F0',
|
||||||
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
{company.websiteUrl && (
|
||||||
|
<a href={company.websiteUrl} target="_blank" rel="noopener noreferrer"
|
||||||
|
style={{ fontSize: 12, color: isDark ? 'rgba(255,255,255,0.5)' : '#263238', textDecoration: 'none' }}>
|
||||||
|
وبسایت ↗
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={`${SITE_URL}/directory/${slug}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ fontSize: 12, color: '#D49A2A', textDecoration: 'none', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
پروفایل در تکناو ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user