Add Instagram post fetching functionality and local JSON fallback
- Introduced a new TypeScript file `bu-sunaidah-fetch.ts` to handle fetching the latest Instagram posts for Bu Sunaidah. - Implemented a scraping method to attempt to retrieve posts directly from Instagram, with a fallback to a local JSON file `bu-sunaidah-posts.json`. - Added a new JSON file containing recent Instagram post URLs and captions. - Included a new image asset `unitree-l2.jpg` for accessories.
66
.scrape/d1t.html
Normal file
76
.scrape/dex11.html
Normal file
66
.scrape/dex25.html
Normal file
66
.scrape/dex31.html
Normal file
72
.scrape/l2.html
Normal file
70
.scrape/z1.html
Normal file
1
public/bu-sunaidah/README.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Drop the approved Bu Sunaidah photo here as portrait.jpg (or .webp).
|
||||||
BIN
public/images/accessories/unitree-d1-t.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 140 KiB |
BIN
public/images/accessories/unitree-dex1-1/diagram.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 441 KiB |
BIN
public/images/accessories/unitree-dex3-1.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
public/images/accessories/unitree-dex3-1/diagram.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
public/images/accessories/unitree-dex5-1/hero.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
public/images/accessories/unitree-dex5-1/params.png
Normal file
|
After Width: | Height: | Size: 361 KiB |
BIN
public/images/accessories/unitree-l2.jpg
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
@ -107,6 +107,122 @@ export default async function AccessoryDetailPage({ params }: { params: Promise<
|
|||||||
</section>
|
</section>
|
||||||
</MotionSection>
|
</MotionSection>
|
||||||
|
|
||||||
|
{/* HIGHLIGHTS */}
|
||||||
|
{accessory.highlights && accessory.highlights.length > 0 && (
|
||||||
|
<MotionSection>
|
||||||
|
<ul className="acc-highlights" role="list">
|
||||||
|
{accessory.highlights.map((h) => (
|
||||||
|
<li key={h.label} className="acc-highlight">
|
||||||
|
<span className="acc-highlight-value">{h.value}</span>
|
||||||
|
<span className="acc-highlight-label">{h.label}</span>
|
||||||
|
{h.sub && <span className="acc-highlight-sub">{h.sub}</span>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</MotionSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* RICH FEATURE SECTIONS — full-bleed alternating */}
|
||||||
|
{accessory.featureSections && accessory.featureSections.length > 0 && (
|
||||||
|
<div className="acc-features-stack">
|
||||||
|
{accessory.featureSections.map((s, i) => {
|
||||||
|
const reverse = i % 2 === 1;
|
||||||
|
const hasImage = Boolean(s.image);
|
||||||
|
return (
|
||||||
|
<MotionSection key={i}>
|
||||||
|
<article className={`acc-feature${reverse ? ' is-reverse' : ''}${hasImage ? '' : ' is-text-only'}`}>
|
||||||
|
{hasImage && (
|
||||||
|
<div className="acc-feature-media">
|
||||||
|
<Image
|
||||||
|
src={s.image!}
|
||||||
|
alt={s.imageAlt ?? s.title}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 820px) 100vw, 50vw"
|
||||||
|
style={{ objectFit: 'cover', objectPosition: 'center' }}
|
||||||
|
/>
|
||||||
|
<span className="acc-feature-media-overlay" aria-hidden />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="acc-feature-body">
|
||||||
|
{s.eyebrow && <span className="eyebrow">{s.eyebrow}</span>}
|
||||||
|
<h2>{s.title}</h2>
|
||||||
|
<p>{s.body}</p>
|
||||||
|
{s.bullets && s.bullets.length > 0 && (
|
||||||
|
<ul className="acc-feature-bullets" role="list">
|
||||||
|
{s.bullets.map((b) => (
|
||||||
|
<li key={b}>
|
||||||
|
<span className="acc-feature-check"><Check size={12} strokeWidth={2.4} /></span>
|
||||||
|
{b}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</MotionSection>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PARAMS DIAGRAM */}
|
||||||
|
{accessory.paramsImage && (
|
||||||
|
<MotionSection>
|
||||||
|
<figure className="acc-params-figure">
|
||||||
|
<Image
|
||||||
|
src={accessory.paramsImage}
|
||||||
|
alt={`${accessory.name} dimensions and parameters diagram`}
|
||||||
|
width={2000}
|
||||||
|
height={859}
|
||||||
|
sizes="(max-width: 920px) 100vw, 1080px"
|
||||||
|
style={{ width: '100%', height: 'auto', display: 'block' }}
|
||||||
|
/>
|
||||||
|
<figcaption>{accessory.name} — body dimensions and structural overview.</figcaption>
|
||||||
|
</figure>
|
||||||
|
</MotionSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SPEC GROUPS */}
|
||||||
|
{accessory.specGroups && accessory.specGroups.length > 0 && (
|
||||||
|
<MotionSection>
|
||||||
|
<section className="acc-specs">
|
||||||
|
<header>
|
||||||
|
<span className="eyebrow">Technical specifications</span>
|
||||||
|
<h2>Full parameter sheet.</h2>
|
||||||
|
</header>
|
||||||
|
<div className="acc-spec-stack">
|
||||||
|
{accessory.specGroups.map((g) => (
|
||||||
|
<div key={g.title} className="acc-spec-group">
|
||||||
|
<div className="acc-spec-head">
|
||||||
|
<h3>{g.title}</h3>
|
||||||
|
{g.variants && g.variants.length > 0 && (
|
||||||
|
<div className="acc-spec-variants" role="row">
|
||||||
|
{g.variants.map((v) => (
|
||||||
|
<span key={v} className="acc-spec-variant">{v}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<table className="acc-spec-table">
|
||||||
|
<tbody>
|
||||||
|
{g.rows.map((r) => (
|
||||||
|
<tr key={r.label}>
|
||||||
|
<th scope="row">{r.label}</th>
|
||||||
|
<td>{r.value}</td>
|
||||||
|
{g.variants && g.variants.length > 1 && (
|
||||||
|
<td>{r.valueAlt ?? r.value}</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</MotionSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* FEATURES + COMPATIBILITY */}
|
{/* FEATURES + COMPATIBILITY */}
|
||||||
<MotionSection>
|
<MotionSection>
|
||||||
<div className="acc-grid">
|
<div className="acc-grid">
|
||||||
@ -199,6 +315,30 @@ export default async function AccessoryDetailPage({ params }: { params: Promise<
|
|||||||
</section>
|
</section>
|
||||||
</MotionSection>
|
</MotionSection>
|
||||||
|
|
||||||
|
{/* FOOTNOTES + SAFETY */}
|
||||||
|
{(accessory.footnotes?.length || accessory.safetyNotice) && (
|
||||||
|
<MotionSection>
|
||||||
|
<aside className="acc-foot">
|
||||||
|
{accessory.footnotes && accessory.footnotes.length > 0 && (
|
||||||
|
<div className="acc-foot-block">
|
||||||
|
<span className="eyebrow">Notes</span>
|
||||||
|
<ol className="acc-foot-list">
|
||||||
|
{accessory.footnotes.map((f, i) => (
|
||||||
|
<li key={i}><sup>{i + 1}</sup> {f}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{accessory.safetyNotice && (
|
||||||
|
<div className="acc-foot-block">
|
||||||
|
<span className="eyebrow">Safety</span>
|
||||||
|
<p>{accessory.safetyNotice}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</MotionSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* RELATED */}
|
{/* RELATED */}
|
||||||
{related.length > 0 && (
|
{related.length > 0 && (
|
||||||
<MotionSection>
|
<MotionSection>
|
||||||
@ -447,6 +587,226 @@ export default async function AccessoryDetailPage({ params }: { params: Promise<
|
|||||||
.acc-hero-actions .cta-btn { width: 100%; justify-content: space-between; }
|
.acc-hero-actions .cta-btn { width: 100%; justify-content: space-between; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* HIGHLIGHTS */
|
||||||
|
.acc-highlights {
|
||||||
|
list-style: none; margin: 0; padding: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
@media (min-width: 820px) { .acc-highlights { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
||||||
|
.acc-highlight {
|
||||||
|
position: relative;
|
||||||
|
display: flex; flex-direction: column; gap: 0.35rem;
|
||||||
|
padding: 1.1rem 1.15rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(120, 140, 255, 0.22);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 100% 0%, color-mix(in srgb, var(--acc) 16%, transparent), transparent 60%),
|
||||||
|
linear-gradient(135deg, rgba(80, 110, 255, 0.10), rgba(255, 255, 255, 0.03));
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.3s, transform 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
.acc-highlight::before {
|
||||||
|
content: ''; position: absolute;
|
||||||
|
top: 0; right: 16px; width: 28px; height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(180, 195, 255, 0.55));
|
||||||
|
}
|
||||||
|
.acc-highlight:hover {
|
||||||
|
border-color: rgba(120, 140, 255, 0.55);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.45), 0 0 22px color-mix(in srgb, var(--acc) 18%, transparent);
|
||||||
|
}
|
||||||
|
.acc-highlight-value {
|
||||||
|
font-size: clamp(1.7rem, 3.2vw, 2.4rem);
|
||||||
|
font-weight: 500; letter-spacing: -0.03em; line-height: 1;
|
||||||
|
background: linear-gradient(180deg, #FFFFFF, #B5BDDB);
|
||||||
|
-webkit-background-clip: text; background-clip: text; color: transparent;
|
||||||
|
}
|
||||||
|
.acc-highlight-label {
|
||||||
|
font-size: 0.66rem; letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase; font-weight: 800; color: #DEE0F0;
|
||||||
|
}
|
||||||
|
.acc-highlight-sub {
|
||||||
|
font-size: 0.78rem; color: #8891C7; line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FEATURE SECTIONS — full-bleed alternating */
|
||||||
|
.acc-features-stack {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
gap: clamp(2rem, 4vw, 3rem);
|
||||||
|
}
|
||||||
|
.acc-feature {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: clamp(1.25rem, 3vw, 2rem);
|
||||||
|
padding: clamp(1.5rem, 3vw, 2.25rem);
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(74, 102, 216, 0.20);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 70% 80% at 0% 100%, color-mix(in srgb, var(--acc) 16%, transparent), transparent 60%),
|
||||||
|
linear-gradient(135deg, rgba(18, 17, 26, 0.95), rgba(8, 8, 12, 0.97));
|
||||||
|
box-shadow: 0 22px 50px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
@media (min-width: 820px) {
|
||||||
|
.acc-feature { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); }
|
||||||
|
.acc-feature.is-reverse { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); }
|
||||||
|
.acc-feature.is-reverse .acc-feature-media { order: 2; }
|
||||||
|
.acc-feature.is-reverse .acc-feature-body { order: 1; }
|
||||||
|
.acc-feature.is-text-only { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
}
|
||||||
|
.acc-feature.is-text-only .acc-feature-media { display: none; }
|
||||||
|
.acc-feature-media {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16 / 11;
|
||||||
|
border-radius: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #0a0a0e;
|
||||||
|
border: 1px solid rgba(222, 224, 240, 0.08);
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.acc-feature-media-overlay {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(10, 10, 14, 0.08) 0%, rgba(10, 10, 14, 0.02) 40%, rgba(10, 10, 14, 0.55) 100%),
|
||||||
|
radial-gradient(ellipse 60% 70% at 100% 0%, color-mix(in srgb, var(--acc) 18%, transparent), transparent 65%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.acc-feature-body {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: clamp(0.25rem, 1vw, 0.75rem);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.acc-feature-body h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.6rem, 3.2vw, 2.4rem);
|
||||||
|
font-weight: 400; letter-spacing: -0.025em;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.acc-feature-body p {
|
||||||
|
margin: 0; color: #DEE0F0;
|
||||||
|
font-size: clamp(0.95rem, 1.8vw, 1.05rem);
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
.acc-feature-bullets {
|
||||||
|
list-style: none; margin: 0.55rem 0 0; padding: 0;
|
||||||
|
display: flex; flex-direction: column; gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.acc-feature-bullets li {
|
||||||
|
display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.65rem;
|
||||||
|
color: #DEE0F0; font-size: 0.92rem; line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PARAMS DIAGRAM */
|
||||||
|
.acc-params-figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: clamp(1.2rem, 2.4vw, 1.8rem);
|
||||||
|
border-radius: 22px;
|
||||||
|
border: 1px solid rgba(74, 102, 216, 0.22);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 50% 0%, color-mix(in srgb, var(--acc) 12%, transparent), transparent 60%),
|
||||||
|
linear-gradient(180deg, rgba(14, 13, 22, 0.95), rgba(8, 8, 12, 0.97));
|
||||||
|
display: flex; flex-direction: column; gap: 1rem;
|
||||||
|
box-shadow: 0 22px 52px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
.acc-params-figure figcaption {
|
||||||
|
color: #8891C7; font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.04em; text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SPEC GROUPS */
|
||||||
|
.acc-specs { display: flex; flex-direction: column; gap: 1.25rem; }
|
||||||
|
.acc-specs header { display: flex; flex-direction: column; gap: 0.45rem; }
|
||||||
|
.acc-specs h2 {
|
||||||
|
margin: 0; font-size: clamp(1.4rem, 2.8vw, 1.85rem);
|
||||||
|
font-weight: 400; letter-spacing: -0.02em; color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.acc-spec-stack {
|
||||||
|
display: flex; flex-direction: column; gap: 1rem;
|
||||||
|
}
|
||||||
|
.acc-spec-group {
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(222, 224, 240, 0.10);
|
||||||
|
background: linear-gradient(180deg, rgba(20, 19, 26, 0.85), rgba(10, 10, 14, 0.94));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.acc-spec-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 0.75rem; padding: 0.9rem 1.1rem;
|
||||||
|
border-bottom: 1px solid rgba(74, 102, 216, 0.18);
|
||||||
|
background: linear-gradient(180deg, rgba(74, 102, 216, 0.08), rgba(20, 19, 26, 0.6));
|
||||||
|
}
|
||||||
|
.acc-spec-head h3 {
|
||||||
|
margin: 0; font-size: 0.95rem; font-weight: 600;
|
||||||
|
color: #FFFFFF; letter-spacing: -0.005em;
|
||||||
|
}
|
||||||
|
.acc-spec-variants {
|
||||||
|
display: flex; gap: 0.4rem; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.acc-spec-variant {
|
||||||
|
font-size: 0.6rem; font-weight: 800;
|
||||||
|
letter-spacing: 0.22em; text-transform: uppercase;
|
||||||
|
color: color-mix(in srgb, var(--acc) 70%, white);
|
||||||
|
padding: 0.22rem 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--acc) 38%, transparent);
|
||||||
|
background: rgba(10, 10, 14, 0.6);
|
||||||
|
}
|
||||||
|
.acc-spec-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
.acc-spec-table th,
|
||||||
|
.acc-spec-table td {
|
||||||
|
padding: 0.7rem 1.1rem;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
border-bottom: 1px solid rgba(222, 224, 240, 0.06);
|
||||||
|
}
|
||||||
|
.acc-spec-table tr:last-child th,
|
||||||
|
.acc-spec-table tr:last-child td { border-bottom: none; }
|
||||||
|
.acc-spec-table th {
|
||||||
|
color: #A6B2D8;
|
||||||
|
font-weight: 600;
|
||||||
|
width: 38%;
|
||||||
|
}
|
||||||
|
.acc-spec-table td {
|
||||||
|
color: #DEE0F0;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOOTNOTES + SAFETY */
|
||||||
|
.acc-foot {
|
||||||
|
display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem;
|
||||||
|
padding: clamp(1.2rem, 2.4vw, 1.8rem);
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(222, 224, 240, 0.10);
|
||||||
|
background: linear-gradient(180deg, rgba(18, 17, 24, 0.85), rgba(10, 10, 14, 0.94));
|
||||||
|
}
|
||||||
|
@media (min-width: 820px) { .acc-foot { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 1.5rem; } }
|
||||||
|
.acc-foot-block { display: flex; flex-direction: column; gap: 0.55rem; }
|
||||||
|
.acc-foot-list {
|
||||||
|
list-style: none; margin: 0; padding: 0;
|
||||||
|
display: flex; flex-direction: column; gap: 0.45rem;
|
||||||
|
}
|
||||||
|
.acc-foot-list li {
|
||||||
|
color: #C9CCDE; font-size: 0.82rem; line-height: 1.55;
|
||||||
|
}
|
||||||
|
.acc-foot-list sup {
|
||||||
|
color: color-mix(in srgb, var(--acc) 70%, white);
|
||||||
|
font-weight: 800;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.acc-foot-block p { margin: 0; color: #C9CCDE; font-size: 0.82rem; line-height: 1.55; }
|
||||||
|
|
||||||
/* RELATED */
|
/* RELATED */
|
||||||
.acc-related { display: flex; flex-direction: column; gap: 1.25rem; }
|
.acc-related { display: flex; flex-direction: column; gap: 1.25rem; }
|
||||||
.acc-related header { display: flex; flex-direction: column; gap: 0.45rem; }
|
.acc-related header { display: flex; flex-direction: column; gap: 0.45rem; }
|
||||||
|
|||||||
@ -19,10 +19,13 @@ import { InstagramGlyph } from '@/components/icons/InstagramGlyph';
|
|||||||
import {
|
import {
|
||||||
BU_SUNAIDAH_HANDLE,
|
BU_SUNAIDAH_HANDLE,
|
||||||
BU_SUNAIDAH_URL,
|
BU_SUNAIDAH_URL,
|
||||||
BU_SUNAIDAH_POSTS,
|
|
||||||
BU_SUNAIDAH_REEL,
|
BU_SUNAIDAH_REEL,
|
||||||
} from '@/data/bu-sunaidah';
|
} from '@/data/bu-sunaidah';
|
||||||
import { BU_SUNAIDAH_PRESS } from '@/data/bu-sunaidah-press';
|
import { BU_SUNAIDAH_PRESS } from '@/data/bu-sunaidah-press';
|
||||||
|
import { getLatestBuSunaidahPosts } from '@/lib/bu-sunaidah-fetch';
|
||||||
|
|
||||||
|
// Re-fetch latest Instagram posts every 30 minutes on the server.
|
||||||
|
export const revalidate = 1800;
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Bu Sunaidah · Emirati Robotics Persona | YS Lootah Robotics',
|
title: 'Bu Sunaidah · Emirati Robotics Persona | YS Lootah Robotics',
|
||||||
@ -74,7 +77,8 @@ function formatDate(iso: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BuSunaidahPage() {
|
export default async function BuSunaidahPage() {
|
||||||
|
const posts = await getLatestBuSunaidahPosts(9);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@ -203,9 +207,9 @@ export default function BuSunaidahPage() {
|
|||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{BU_SUNAIDAH_POSTS.length > 0 ? (
|
{posts.length > 0 ? (
|
||||||
<div className="bs-ig-grid">
|
<div className="bs-ig-grid">
|
||||||
{BU_SUNAIDAH_POSTS.map((post) => {
|
{posts.map((post) => {
|
||||||
const kind = post.kind ?? 'p';
|
const kind = post.kind ?? 'p';
|
||||||
const src = `https://www.instagram.com/${kind}/${post.shortcode}/embed/`;
|
const src = `https://www.instagram.com/${kind}/${post.shortcode}/embed/`;
|
||||||
return (
|
return (
|
||||||
@ -632,10 +636,22 @@ export default function BuSunaidahPage() {
|
|||||||
border: 1px solid rgba(222, 224, 240, 0.12);
|
border: 1px solid rgba(222, 224, 240, 0.12);
|
||||||
background: rgba(14, 13, 18, 0.6);
|
background: rgba(14, 13, 18, 0.6);
|
||||||
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.45);
|
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.45);
|
||||||
aspect-ratio: 4 / 5;
|
aspect-ratio: 9 / 16;
|
||||||
|
min-height: 620px;
|
||||||
}
|
}
|
||||||
|
/* The Instagram embed iframe ships an in-frame header (avatar +
|
||||||
|
username + "View profile") at the top. It is cross-origin so we
|
||||||
|
can't hide it from inside. Workaround: shift the iframe up by
|
||||||
|
~54px and let the rounded parent clip it. Adjust if Instagram
|
||||||
|
changes header sizing. */
|
||||||
.bs-ig-tile iframe {
|
.bs-ig-tile iframe {
|
||||||
width: 100%; height: 100%; display: block; border: 0;
|
position: absolute;
|
||||||
|
top: -54px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% + 54px);
|
||||||
|
display: block;
|
||||||
|
border: 0;
|
||||||
background: #0a0a0e;
|
background: #0a0a0e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Navbar } from '@/components/Navbar';
|
|||||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||||
import { CatalogClient } from './CatalogClient';
|
import { CatalogClient } from './CatalogClient';
|
||||||
import { AccessoriesShowcase } from '@/components/robotics/AccessoriesShowcase';
|
import { AccessoriesShowcase } from '@/components/robotics/AccessoriesShowcase';
|
||||||
|
import { RoboticsSplineShowcase } from '@/components/sections/robotics-spline-showcase';
|
||||||
import { CTAButton } from '@/components/ui/CTAButton';
|
import { CTAButton } from '@/components/ui/CTAButton';
|
||||||
import { ROBOTS } from '@/data/robots';
|
import { ROBOTS } from '@/data/robots';
|
||||||
import { ACCESSORIES } from '@/data/accessories';
|
import { ACCESSORIES } from '@/data/accessories';
|
||||||
@ -32,6 +33,11 @@ export default function RobotsPage() {
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<main className="rb-main">
|
<main className="rb-main">
|
||||||
|
{/* SPLINE SHOWCASE — top-of-page robot + hand animation */}
|
||||||
|
<div className="rb-spline-wrap">
|
||||||
|
<RoboticsSplineShowcase />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="container-wide rb-stack">
|
<div className="container-wide rb-stack">
|
||||||
{/* BREADCRUMBS */}
|
{/* BREADCRUMBS */}
|
||||||
<nav aria-label="Breadcrumb" className="rb-crumbs">
|
<nav aria-label="Breadcrumb" className="rb-crumbs">
|
||||||
@ -126,6 +132,9 @@ export default function RobotsPage() {
|
|||||||
padding-top: clamp(5rem, 9vw, 7rem);
|
padding-top: clamp(5rem, 9vw, 7rem);
|
||||||
padding-bottom: clamp(3rem, 6vw, 5rem);
|
padding-bottom: clamp(3rem, 6vw, 5rem);
|
||||||
}
|
}
|
||||||
|
.rb-spline-wrap {
|
||||||
|
margin-bottom: clamp(2rem, 4vw, 3rem);
|
||||||
|
}
|
||||||
.rb-stack {
|
.rb-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -37,8 +37,8 @@ export function AccessoriesShowcase() {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: '1rem',
|
gap: '1.25rem',
|
||||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(260px, 100%), 1fr))',
|
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{list.map((accessory) => (
|
{list.map((accessory) => (
|
||||||
@ -83,141 +83,209 @@ function FilterChip({
|
|||||||
|
|
||||||
function AccessoryCard({ accessory }: { accessory: Accessory }) {
|
function AccessoryCard({ accessory }: { accessory: Accessory }) {
|
||||||
const accent = accessory.accent;
|
const accent = accessory.accent;
|
||||||
|
const detailHref = `/accessories/${accessory.slug}/`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<article
|
||||||
href={`/accessories/${accessory.slug}/`}
|
className="card"
|
||||||
aria-label={`View details for ${accessory.name}`}
|
style={{
|
||||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
color: '#FBFBFD',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<article
|
<Link
|
||||||
className="acc-card"
|
href={detailHref}
|
||||||
|
aria-label={`View details for ${accessory.brandLabel} ${accessory.name}`}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'block',
|
||||||
flexDirection: 'column',
|
aspectRatio: '1 / 1',
|
||||||
height: '100%',
|
|
||||||
borderRadius: 18,
|
|
||||||
overflow: 'hidden',
|
|
||||||
border: '1px solid rgba(222,224,240,0.10)',
|
|
||||||
background:
|
background:
|
||||||
`radial-gradient(ellipse 80% 60% at 100% 0%, ${accent}1A, transparent 60%), linear-gradient(180deg, rgba(22,21,30,0.92), rgba(10,10,14,0.96))`,
|
`radial-gradient(ellipse 70% 50% at 50% 60%, ${accent}26 0%, transparent 60%), linear-gradient(180deg, rgba(15, 12, 8,0.7), rgba(5, 5, 5,0.95))`,
|
||||||
boxShadow: '0 1px 0 rgba(222,224,240,0.04) inset, 0 14px 28px rgba(0,0,0,0.36)',
|
overflow: 'hidden',
|
||||||
transition: 'transform 0.4s cubic-bezier(0.16,1,0.3,1), border-color 0.4s, box-shadow 0.4s',
|
textDecoration: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'absolute',
|
||||||
aspectRatio: '16 / 11',
|
inset: 0,
|
||||||
background: '#0a0a0e',
|
backgroundImage:
|
||||||
borderBottom: '1px solid rgba(222,224,240,0.06)',
|
'linear-gradient(rgba(39, 63, 148,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(39, 63, 148,0.05) 1px, transparent 1px)',
|
||||||
overflow: 'hidden',
|
backgroundSize: '32px 32px',
|
||||||
|
maskImage: 'radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 80%)',
|
||||||
|
WebkitMaskImage: 'radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 80%)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src={accessory.image}
|
||||||
|
alt={`${accessory.brandLabel} ${accessory.name}`}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||||
|
style={{
|
||||||
|
objectFit: 'contain',
|
||||||
|
padding: '2rem',
|
||||||
|
transition: 'transform 0.6s cubic-bezier(0.16,1,0.3,1)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '1rem',
|
||||||
|
left: '1rem',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
padding: '0.4rem 0.7rem',
|
||||||
|
borderRadius: '999px',
|
||||||
|
background: 'rgba(5, 5, 5,0.7)',
|
||||||
|
border: `1px solid ${accent}55`,
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
letterSpacing: '0.18em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: '#e8e0cf',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<span style={{ width: 6, height: 6, borderRadius: 999, background: accent }} />
|
||||||
src={accessory.image}
|
{accessory.brandLabel}
|
||||||
alt={`${accessory.name} product image`}
|
|
||||||
fill
|
|
||||||
sizes="(max-width: 600px) 100vw, (max-width: 1100px) 50vw, 25vw"
|
|
||||||
style={{ objectFit: 'contain', objectPosition: 'center', padding: '0.6rem' }}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
inset: 0,
|
|
||||||
background: `radial-gradient(ellipse 60% 70% at 50% 40%, ${accent}26, transparent 65%)`,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0.55rem',
|
|
||||||
left: '0.55rem',
|
|
||||||
padding: '0.22rem 0.55rem',
|
|
||||||
borderRadius: 999,
|
|
||||||
fontSize: '0.55rem',
|
|
||||||
fontWeight: 800,
|
|
||||||
letterSpacing: '0.22em',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
color: accent,
|
|
||||||
background: 'rgba(10,10,14,0.7)',
|
|
||||||
border: `1px solid ${accent}55`,
|
|
||||||
backdropFilter: 'blur(8px)',
|
|
||||||
WebkitBackdropFilter: 'blur(8px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{accessory.brandLabel}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0.55rem',
|
|
||||||
right: '0.55rem',
|
|
||||||
padding: '0.22rem 0.55rem',
|
|
||||||
borderRadius: 999,
|
|
||||||
fontSize: '0.55rem',
|
|
||||||
fontWeight: 800,
|
|
||||||
letterSpacing: '0.18em',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
color: '#DEE0F0',
|
|
||||||
background: 'rgba(10,10,14,0.7)',
|
|
||||||
border: '1px solid rgba(222,224,240,0.14)',
|
|
||||||
backdropFilter: 'blur(8px)',
|
|
||||||
WebkitBackdropFilter: 'blur(8px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{GROUP_LABELS[accessory.group]}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '1rem',
|
||||||
|
right: '1rem',
|
||||||
|
padding: '0.35rem 0.7rem',
|
||||||
|
borderRadius: '999px',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(39, 63, 148,0.2)',
|
||||||
|
fontSize: '0.66rem',
|
||||||
|
letterSpacing: '0.18em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: '#DEE0F0',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{GROUP_LABELS[accessory.group]}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.55rem', padding: '1rem 1.1rem 1.1rem', flex: 1 }}>
|
<div style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '0.75rem', flex: 1 }}>
|
||||||
<h3 style={{ margin: 0, fontSize: '1.02rem', fontWeight: 600, letterSpacing: '-0.005em', color: '#FFFFFF' }}>
|
<h3 style={{ margin: 0, fontSize: '1.2rem', fontWeight: 600, letterSpacing: '-0.01em' }}>
|
||||||
|
<Link href={detailHref} style={{ color: '#FBFBFD', textDecoration: 'none' }}>
|
||||||
{accessory.name}
|
{accessory.name}
|
||||||
</h3>
|
</Link>
|
||||||
<p style={{ margin: 0, fontSize: '0.84rem', color: '#C9CCDE', lineHeight: 1.55, flex: 1 }}>
|
</h3>
|
||||||
{accessory.tagline}
|
<p style={{ margin: 0, color: '#8891C7', fontSize: '0.88rem', lineHeight: 1.55, flex: 1 }}>
|
||||||
</p>
|
{accessory.tagline}
|
||||||
{accessory.compatibility && accessory.compatibility.length > 0 && (
|
</p>
|
||||||
<div style={{ paddingTop: '0.55rem', borderTop: '1px solid rgba(222,224,240,0.06)' }}>
|
|
||||||
<span style={{ display: 'block', fontSize: '0.55rem', fontWeight: 800, letterSpacing: '0.24em', textTransform: 'uppercase', color: '#8891C7', marginBottom: '0.35rem' }}>
|
|
||||||
Compatible with
|
|
||||||
</span>
|
|
||||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
|
||||||
{accessory.compatibility.slice(0, 4).map((c) => (
|
|
||||||
<li
|
|
||||||
key={c}
|
|
||||||
style={{
|
|
||||||
fontSize: '0.66rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
padding: '0.18rem 0.5rem',
|
|
||||||
borderRadius: 999,
|
|
||||||
border: '1px solid rgba(222,224,240,0.10)',
|
|
||||||
background: 'rgba(20,19,26,0.7)',
|
|
||||||
color: '#DEE0F0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{c}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<style jsx>{`
|
{accessory.compatibility && accessory.compatibility.length > 0 && (
|
||||||
:global(.acc-card:hover),
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
|
||||||
:global(.acc-card:focus-visible) {
|
{accessory.compatibility.slice(0, 2).map((c) => (
|
||||||
transform: translateY(-3px);
|
<li
|
||||||
border-color: ${accent}66 !important;
|
key={c}
|
||||||
box-shadow:
|
style={{
|
||||||
0 1px 0 rgba(222,224,240,0.08) inset,
|
fontSize: '0.7rem',
|
||||||
0 24px 52px rgba(0,0,0,0.55),
|
color: '#8891C7',
|
||||||
0 0 28px ${accent}33 !important;
|
padding: '0.2rem 0.5rem',
|
||||||
}
|
borderRadius: 999,
|
||||||
`}</style>
|
border: '1px solid rgba(39, 63, 148,0.18)',
|
||||||
</Link>
|
background: 'rgba(15, 12, 8,0.5)',
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{c}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 'auto',
|
||||||
|
paddingTop: '0.75rem',
|
||||||
|
borderTop: '1px solid rgba(39, 63, 148,0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '0.4rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={detailHref}
|
||||||
|
className="card-cta card-cta-primary"
|
||||||
|
style={{
|
||||||
|
flex: '1 1 auto',
|
||||||
|
minWidth: 0,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '0.4rem',
|
||||||
|
padding: '0.55rem 0.75rem',
|
||||||
|
borderRadius: 10,
|
||||||
|
background: 'linear-gradient(135deg, rgba(80, 110, 255, 0.32), rgba(40, 55, 145, 0.85))',
|
||||||
|
border: '1px solid rgba(120, 140, 255, 0.55)',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '0.12em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
textDecoration: 'none',
|
||||||
|
boxShadow: '0 6px 14px rgba(0, 0, 0, 0.35), 0 0 14px rgba(74, 102, 216, 0.22)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View details
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`${detailHref}#inquire`}
|
||||||
|
style={{
|
||||||
|
flex: '1 1 auto',
|
||||||
|
minWidth: 0,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '0.55rem 0.75rem',
|
||||||
|
borderRadius: 10,
|
||||||
|
background: 'rgba(20, 19, 26, 0.6)',
|
||||||
|
border: '1px solid rgba(222, 224, 240, 0.14)',
|
||||||
|
color: '#DEE0F0',
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Request price
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/book-demo/"
|
||||||
|
style={{
|
||||||
|
flex: '1 1 auto',
|
||||||
|
minWidth: 0,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '0.55rem 0.75rem',
|
||||||
|
borderRadius: 10,
|
||||||
|
background: 'rgba(20, 19, 26, 0.6)',
|
||||||
|
border: '1px solid rgba(222, 224, 240, 0.14)',
|
||||||
|
color: '#DEE0F0',
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Book demo
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,14 +153,15 @@ export function RobotProductCard({ robot, priority = false }: Props) {
|
|||||||
gap: '0.4rem',
|
gap: '0.4rem',
|
||||||
padding: '0.55rem 0.75rem',
|
padding: '0.55rem 0.75rem',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
background: `${robot.accent}1F`,
|
background: 'linear-gradient(135deg, rgba(80, 110, 255, 0.32), rgba(40, 55, 145, 0.85))',
|
||||||
border: `1px solid ${robot.accent}66`,
|
border: '1px solid rgba(120, 140, 255, 0.55)',
|
||||||
color: robot.accent,
|
color: '#FFFFFF',
|
||||||
fontSize: '0.72rem',
|
fontSize: '0.72rem',
|
||||||
fontWeight: 600,
|
fontWeight: 700,
|
||||||
letterSpacing: '0.1em',
|
letterSpacing: '0.12em',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
|
boxShadow: '0 6px 14px rgba(0, 0, 0, 0.35), 0 0 14px rgba(74, 102, 216, 0.22)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View details
|
View details
|
||||||
@ -175,8 +176,8 @@ export function RobotProductCard({ robot, priority = false }: Props) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: '0.55rem 0.75rem',
|
padding: '0.55rem 0.75rem',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
background: 'transparent',
|
background: 'rgba(20, 19, 26, 0.6)',
|
||||||
border: '1px solid rgba(39, 63, 148,0.2)',
|
border: '1px solid rgba(222, 224, 240, 0.14)',
|
||||||
color: '#DEE0F0',
|
color: '#DEE0F0',
|
||||||
fontSize: '0.72rem',
|
fontSize: '0.72rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -197,8 +198,8 @@ export function RobotProductCard({ robot, priority = false }: Props) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: '0.55rem 0.75rem',
|
padding: '0.55rem 0.75rem',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
background: 'transparent',
|
background: 'rgba(20, 19, 26, 0.6)',
|
||||||
border: '1px solid rgba(39, 63, 148,0.2)',
|
border: '1px solid rgba(222, 224, 240, 0.14)',
|
||||||
color: '#DEE0F0',
|
color: '#DEE0F0',
|
||||||
fontSize: '0.72rem',
|
fontSize: '0.72rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
|||||||
@ -8,6 +8,33 @@ export type AccessoryGroup =
|
|||||||
| 'attachment'
|
| 'attachment'
|
||||||
| 'iot-integration';
|
| 'iot-integration';
|
||||||
|
|
||||||
|
export interface AccessoryHighlight {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
sub?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessoryFeatureSection {
|
||||||
|
eyebrow?: string;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
bullets?: string[];
|
||||||
|
image?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpecRow {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
valueAlt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpecGroup {
|
||||||
|
title: string;
|
||||||
|
variants?: string[];
|
||||||
|
rows: SpecRow[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Accessory {
|
export interface Accessory {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@ -22,6 +49,14 @@ export interface Accessory {
|
|||||||
accent: string;
|
accent: string;
|
||||||
officialUrl?: string;
|
officialUrl?: string;
|
||||||
compatibility?: string[];
|
compatibility?: string[];
|
||||||
|
|
||||||
|
/* Optional rich content */
|
||||||
|
highlights?: AccessoryHighlight[];
|
||||||
|
featureSections?: AccessoryFeatureSection[];
|
||||||
|
specGroups?: SpecGroup[];
|
||||||
|
paramsImage?: string;
|
||||||
|
footnotes?: string[];
|
||||||
|
safetyNotice?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GROUP_LABELS: Record<AccessoryGroup, string> = {
|
export const GROUP_LABELS: Record<AccessoryGroup, string> = {
|
||||||
@ -46,10 +81,10 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
brand: 'unitree',
|
brand: 'unitree',
|
||||||
brandLabel: 'Unitree',
|
brandLabel: 'Unitree',
|
||||||
name: 'Dex2-5 Dexterous Hand',
|
name: 'Dex2-5 Dexterous Hand',
|
||||||
tagline: 'Tendon-driven 5-finger hand with 100,000+ fatigue cycles tested.',
|
tagline: 'Gala-style tendon-driven dexterous hand — precise control for unbeatable strength.',
|
||||||
group: 'dexterous-hand',
|
group: 'dexterous-hand',
|
||||||
description:
|
description:
|
||||||
'A 5-finger, 10-DOF tendon-driven dexterous hand for G1 and R1 humanoids. Gear-tendon drive with hidden internal wiring, 365 g lightweight design, and full perceptual feedback at 1 kHz.',
|
'A 10-DOF (2 actuated) 5-finger tendon-driven dexterous hand for G1 and R1 humanoids. Gear-tendon drive with hidden internal wiring, 365 g lightweight design, and full perceptual feedback at 1 kHz.',
|
||||||
features: [
|
features: [
|
||||||
'10 DOF total (2 actuated), 5 fingers',
|
'10 DOF total (2 actuated), 5 fingers',
|
||||||
'365 g, 151×70×63 mm',
|
'365 g, 151×70×63 mm',
|
||||||
@ -62,6 +97,70 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
accent: GOLD_CHAMPAGNE,
|
accent: GOLD_CHAMPAGNE,
|
||||||
officialUrl: 'https://www.unitree.com/Dex2-5',
|
officialUrl: 'https://www.unitree.com/Dex2-5',
|
||||||
compatibility: ['Unitree G1', 'Unitree R1'],
|
compatibility: ['Unitree G1', 'Unitree R1'],
|
||||||
|
highlights: [
|
||||||
|
{ value: '10', label: 'Degrees of freedom', sub: '2 actuated, 5-finger' },
|
||||||
|
{ value: '365g', label: 'Weight', sub: 'Lightweight build' },
|
||||||
|
{ value: '100,000+', label: 'Fatigue cycles', sub: 'Ultra-long fatigue life' },
|
||||||
|
{ value: '100+', label: 'Drop tests passed', sub: 'Rugged impact resistance' },
|
||||||
|
],
|
||||||
|
featureSections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Durability',
|
||||||
|
title: 'Ultra-long fatigue life for greater durability.',
|
||||||
|
body: 'The entire hand has passed over 100,000 fatigue cycles, ensuring stable and reliable long-term operation.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Impact resistance',
|
||||||
|
title: 'Rugged design — impervious to knocks and bumps.',
|
||||||
|
body: 'Completed over 100 drop tests. Superior robust design ensures the dexterous hand is no longer fragile.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Integration',
|
||||||
|
title: 'Hidden internal wiring.',
|
||||||
|
body: 'Compatible with G1 and R1 humanoids. The “meridians” run beneath the “skin” for a clean, durable mechanical envelope.',
|
||||||
|
bullets: ['Compatible with Unitree G1', 'Compatible with Unitree R1', 'Lightweight and compact'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
specGroups: [
|
||||||
|
{
|
||||||
|
title: 'Parameters',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Weight', value: '365 g' },
|
||||||
|
{ label: 'Size¹', value: '151 × 70 × 63 mm' },
|
||||||
|
{ label: 'Degrees of freedom', value: 'Thumb × 1; Index, Middle, Ring, Little: share one DOF' },
|
||||||
|
{ label: 'Transmission', value: 'Gear-tendon rope drive' },
|
||||||
|
{ label: 'Thumb joint 0', value: '0° to 42°' },
|
||||||
|
{ label: 'Thumb joint 1', value: '0° to 105°' },
|
||||||
|
{ label: 'Four-finger joint 0', value: '0° to 88°' },
|
||||||
|
{ label: 'Four-finger joint 1', value: '0° to 105°' },
|
||||||
|
{ label: 'Minimum fist clench time', value: '0.5 s' },
|
||||||
|
{ label: 'Minimum grip diameter', value: '8 mm' },
|
||||||
|
{ label: 'Fingertip force', value: 'Thumb 15 N; index/middle/ring/little total 15 N' },
|
||||||
|
{ label: 'Working voltage', value: '24 V – 60 V' },
|
||||||
|
{ label: 'Static current', value: '58 V @ 0.1 A' },
|
||||||
|
{ label: 'Maximum current', value: '58 V @ 1.5 A' },
|
||||||
|
{ label: 'Communication interface', value: 'RS485' },
|
||||||
|
{ label: 'Working temperature range', value: '−20 °C to 60 °C' },
|
||||||
|
{ label: 'Load (palm facing down, 5 cm round hard object)', value: '1.5 kg max' },
|
||||||
|
{ label: 'Load (palm facing left, 5 cm round hard object)', value: '1.5 kg max' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Software Function',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Communication rate', value: '1000 Hz' },
|
||||||
|
{ label: 'Perceptual feedback', value: 'Joint mode, position, velocity, torque, temperature, voltage and current' },
|
||||||
|
{ label: 'Control feedback', value: 'Joint mode, position, velocity, torque, stiffness coefficient, damping coefficient' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
footnotes: [
|
||||||
|
'Size refers to the dexterous hand in flat state.',
|
||||||
|
'Parameters listed above may vary in different application scenarios or with different model configurations.',
|
||||||
|
'Product appearance is subject to change. Refer to the final product.',
|
||||||
|
],
|
||||||
|
safetyNotice:
|
||||||
|
'This product is a civilian robot. Users are requested to refrain from making dangerous modifications or using the robot in a hazardous manner. Comply with local laws and regulations and consult Unitree Robotics terms for full policies.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'unitree-dex5-1',
|
id: 'unitree-dex5-1',
|
||||||
@ -69,22 +168,133 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
brand: 'unitree',
|
brand: 'unitree',
|
||||||
brandLabel: 'Unitree',
|
brandLabel: 'Unitree',
|
||||||
name: 'Dex5-1 Dexterous Hand',
|
name: 'Dex5-1 Dexterous Hand',
|
||||||
tagline: 'High-DOF dexterous hand with 94 tactile sensors for RL workflows.',
|
tagline: 'Smart adaptability, instant responsiveness.',
|
||||||
group: 'dexterous-hand',
|
group: 'dexterous-hand',
|
||||||
description:
|
description:
|
||||||
'A 20-DOF (16 actuated) dexterous hand with 94 tactile sensors and backdrivable joints designed for reinforcement-learning workflows. ±22° four-finger lateral swing and operation from –20 °C to 60 °C.',
|
'A 20-DOF (16 actuated) dexterous hand with 94 tactile sensors and backdrivable joints designed for reinforcement-learning workflows. Each finger joint supports smooth backdrivability, eliminating rigidity for smoother and more convenient operations. ±22° four-finger lateral swing and operation from –20 °C to 60 °C.',
|
||||||
features: [
|
features: [
|
||||||
'20 DOF (16 active + 4)',
|
'20 DOF (16 active + 4)',
|
||||||
'94 tactile sensors',
|
'94 tactile sensors per hand',
|
||||||
'1100 g, –20 °C to 60 °C',
|
'1100 g, 217.3 × 127.5 × 72.1 mm',
|
||||||
'3.5 kg palm-down / 4.5 kg palm-side load',
|
'3.5 kg palm-down / 4.5 kg palm-side payload',
|
||||||
'±22° four-finger lateral swing',
|
'±22° four-finger lateral swing',
|
||||||
'Backdrivable, low-damping reducers',
|
'Hollow-cup motors + high-precision encoders',
|
||||||
|
'Low-damping, small-clearance reducers',
|
||||||
|
'1000 Hz comm + sensor rate',
|
||||||
],
|
],
|
||||||
image: '/images/accessories/unitree-dex5-1.png',
|
image: '/images/accessories/unitree-dex5-1.png',
|
||||||
accent: GOLD_BRAND,
|
accent: GOLD_BRAND,
|
||||||
officialUrl: 'https://www.unitree.com/Dex5-1',
|
officialUrl: 'https://www.unitree.com/Dex5-1',
|
||||||
compatibility: ['Unitree G1', 'Unitree H1', 'Unitree H2', 'Research arms'],
|
compatibility: ['Unitree G1', 'Unitree H1', 'Unitree H2', 'Research arms'],
|
||||||
|
highlights: [
|
||||||
|
{ value: '20', label: 'Degrees of freedom', sub: '16 active + 4' },
|
||||||
|
{ value: '94', label: 'Tactile sensors', sub: 'Per hand' },
|
||||||
|
{ value: '±22°', label: 'Lateral swing', sub: 'Four-finger range' },
|
||||||
|
{ value: '1000 Hz', label: 'Comm rate', sub: 'Real-time control' },
|
||||||
|
],
|
||||||
|
featureSections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Reflexes & flexibility',
|
||||||
|
title: 'Ultra-fast reflexes, snake-like flexibility.',
|
||||||
|
body: 'Twenty degrees of freedom plus five independently replaceable fingers deliver responsive grasps and natural articulation. Each finger joint backdrives smoothly, eliminating rigidity for reinforcement-learning workflows.',
|
||||||
|
bullets: [
|
||||||
|
'20 degrees of freedom',
|
||||||
|
'5 fingers — each replaceable independently',
|
||||||
|
'Backdrivable joints with low damping',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Four-finger lateral swing',
|
||||||
|
title: '±22° lateral range for reliable curved grips.',
|
||||||
|
body: 'The four-finger lateral swing improves grip reliability and adapts more effectively to curved surfaces of grasped objects — bottles, tools, glassware, and irregular shapes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Sensing & feedback',
|
||||||
|
title: 'Precise feedback. Control every millimeter.',
|
||||||
|
body: 'A high-power-density hollow-cup motor pairs with a high-precision encoder and a low-damping, small-clearance reducer. A micro-gap design keeps rotation axes close to the contact surface, preventing corner interference during grasping. Tactile algorithms support secondary development.',
|
||||||
|
bullets: [
|
||||||
|
'94 tactile sensors with palm + fingertip + finger-root arrays',
|
||||||
|
'High-power-density hollow-cup motor',
|
||||||
|
'High-precision encoder',
|
||||||
|
'Micro-gap, low-damping reducers',
|
||||||
|
'Secondary-development support for tactile algorithms',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
specGroups: [
|
||||||
|
{
|
||||||
|
title: 'Body Parameters',
|
||||||
|
variants: ['Dex5-1', 'Dex5-1P'],
|
||||||
|
rows: [
|
||||||
|
{ label: 'Weight', value: '1100 g', valueAlt: '1100 g' },
|
||||||
|
{ label: 'Size¹', value: '217.3 × 127.5 × 72.1 mm', valueAlt: '217.3 × 127.5 × 72.1 mm' },
|
||||||
|
{ label: 'Degrees of freedom', value: 'Thumb × 4, Index × 3, Middle × 3, Ring × 3, Little × 3', valueAlt: 'Thumb × 4, Index × 3, Middle × 3, Ring × 3, Little × 3' },
|
||||||
|
{ label: 'Transmission', value: '12 self-developed micro force-controlled composite joints (tactile proprioception) + 4 micro force-controlled gear joints', valueAlt: '12 self-developed micro force-controlled composite joints (tactile proprioception) + 4 micro force-controlled gear joints' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Joint Angle Ranges',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Thumb joint 0', value: '−33.5° to 39°' },
|
||||||
|
{ label: 'Thumb joint 1', value: '0° to 100°' },
|
||||||
|
{ label: 'Thumb joint 2', value: '0° to 110°' },
|
||||||
|
{ label: 'Thumb joint 3', value: '0° to 92°' },
|
||||||
|
{ label: 'Four-finger knuckle 0', value: '−22° to 22°' },
|
||||||
|
{ label: 'Four-finger knuckle 1', value: '0° to 90°' },
|
||||||
|
{ label: 'Four-finger knuckle 2', value: '0° to 95°' },
|
||||||
|
{ label: 'Four-finger knuckle 3', value: '0° to 81° (coupled with joint 2)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Performance',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Four-finger lateral swing', value: '±22°' },
|
||||||
|
{ label: 'Minimum grip diameter', value: '10 mm' },
|
||||||
|
{ label: 'Fingertip repeat-positioning accuracy', value: '±1 mm' },
|
||||||
|
{ label: 'Fingertip force', value: '10 N' },
|
||||||
|
{ label: 'Working voltage', value: '24 V – 60 V' },
|
||||||
|
{ label: 'Static current', value: '58 V @ 0.2 A' },
|
||||||
|
{ label: 'Maximum current', value: '58 V @ 4 A' },
|
||||||
|
{ label: 'Communication interface', value: 'USB 2.0' },
|
||||||
|
{ label: 'Working temperature range', value: '−20 °C to 60 °C' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Load Conditions²',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Palm facing down (room temp, 5 cm round hard object)', value: '3.5 kg max' },
|
||||||
|
{ label: 'Palm facing left (room temp, 5 cm round hard object)', value: '4.5 kg max' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Sensing Parameters (Dex5-1P)',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Pressure sensors', value: '94 total (palm 12 + finger pads + fingertips + finger roots)' },
|
||||||
|
{ label: 'Array resolution', value: '2×5 palm; 2×3 finger pad ×5; 2×3 fingertip ×5; 2×3 finger root ×4' },
|
||||||
|
{ label: 'Perception range', value: '10 g – 2500 g' },
|
||||||
|
{ label: 'Max acceptance (undamaged)', value: '20 kg' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Software',
|
||||||
|
variants: ['Dex5-1', 'Dex5-1P'],
|
||||||
|
rows: [
|
||||||
|
{ label: 'Communication rate', value: '1000 Hz', valueAlt: '1000 Hz' },
|
||||||
|
{ label: 'Full packet bytes', value: 'Sender 1234 / Receiver 1270', valueAlt: 'Sender 1234 / Receiver 1270' },
|
||||||
|
{ label: 'Perceptual feedback', value: 'Joint mode, position, velocity, torque, temperature, voltage/current, IMU data', valueAlt: 'Above + sensor pressure value, sensor temperature value' },
|
||||||
|
{ label: 'Control feedback', value: 'Joint mode, position, velocity, torque, stiffness coefficient, damping coefficient', valueAlt: 'Joint mode, position, velocity, torque, stiffness coefficient, damping coefficient' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paramsImage: '/images/accessories/unitree-dex5-1/params.png',
|
||||||
|
footnotes: [
|
||||||
|
'Size refers to dexterous hand in flat state.',
|
||||||
|
'Pressure data represents force from a vertically-applied 1 cm diameter cylinder; varies by application scenario.',
|
||||||
|
'Parameters may vary by scenario or configuration; subject to actual situation.',
|
||||||
|
'Product appearance subject to change; refer to the final product.',
|
||||||
|
],
|
||||||
|
safetyNotice:
|
||||||
|
'This product is a civilian robot. Users are requested to refrain from making dangerous modifications or using the robot in a hazardous manner. Comply with local laws and regulations and consult Unitree Robotics terms for full policies.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'unitree-dex3-1',
|
id: 'unitree-dex3-1',
|
||||||
@ -92,22 +302,113 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
brand: 'unitree',
|
brand: 'unitree',
|
||||||
brandLabel: 'Unitree',
|
brandLabel: 'Unitree',
|
||||||
name: 'Dex3-1 Dexterous Hand',
|
name: 'Dex3-1 Dexterous Hand',
|
||||||
tagline: 'Power-control 3-finger hand with hybrid force/position control.',
|
tagline: 'Power control dexterous hand — operate everything.',
|
||||||
group: 'dexterous-hand',
|
group: 'dexterous-hand',
|
||||||
description:
|
description:
|
||||||
'A 3-finger, 7-DOF dexterous hand with 33 tactile pressure sensors and hybrid force/position control. Designed to pair with the G1 humanoid for general-purpose manipulation.',
|
'A 3-finger, 7-DOF dexterous hand with 33 tactile pressure sensors and hybrid force/position control. Sensitive and reliable for precise object manipulation. Pairs natively with the G1 humanoid for complex grasping and manipulation workflows.',
|
||||||
features: [
|
features: [
|
||||||
'7 DOF, 3 fingers',
|
'7 DOF, 3 fingers',
|
||||||
'33 tactile pressure sensors',
|
'6 motors direct drive + 1 gear drive',
|
||||||
|
'33 tactile pressure sensors per hand',
|
||||||
'±2 mm fingertip repeatability',
|
'±2 mm fingertip repeatability',
|
||||||
'500 g payload',
|
'710 g, 175 × 88 × 77 mm',
|
||||||
'12–58 V operating voltage',
|
'12–58 V operating voltage',
|
||||||
'1 kHz communication rate',
|
'1 kHz communication rate',
|
||||||
],
|
],
|
||||||
image: '/images/accessories/unitree-dex3-1.png',
|
image: '/images/accessories/unitree-dex3-1.jpg',
|
||||||
accent: GOLD_BRONZE,
|
accent: GOLD_BRONZE,
|
||||||
officialUrl: 'https://www.unitree.com/Dex3-1',
|
officialUrl: 'https://www.unitree.com/Dex3-1',
|
||||||
compatibility: ['Unitree G1'],
|
compatibility: ['Unitree G1'],
|
||||||
|
highlights: [
|
||||||
|
{ value: '7', label: 'Degrees of freedom', sub: '3-finger dexterous hand' },
|
||||||
|
{ value: '33', label: 'Tactile sensors', sub: '9 pressure-sensor groups' },
|
||||||
|
{ value: '710 g', label: 'Weight', sub: '175 × 88 × 77 mm' },
|
||||||
|
{ value: '±2 mm', label: 'Fingertip accuracy', sub: 'Repeat positioning' },
|
||||||
|
],
|
||||||
|
featureSections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Data collection',
|
||||||
|
title: 'Collect data — sensitive and accurate.',
|
||||||
|
body: 'Open-source data-acquisition workflow for embodied-AI research. Pairs with the G1 humanoid to perform a wide range of complex actions.',
|
||||||
|
bullets: ['Open-source links available', 'Pair with G1 humanoid robot'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Architecture',
|
||||||
|
title: 'Three-finger dexterous hand built for manipulation.',
|
||||||
|
body: 'Dex3-1 combines 7 degrees of freedom with 6 micro brushless force-controlled joint direct drives and 1 micro brushless force-controlled gear drive. Each hand carries 33 tactile sensors. Compatible with G1 to perform complex actions in real workflows.',
|
||||||
|
bullets: ['6 motors direct drive', '1 motor gear drive', '33 tactile sensors per hand', 'Compatible with G1 humanoid'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
specGroups: [
|
||||||
|
{
|
||||||
|
title: 'Body Parameters',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Weight', value: '710 g' },
|
||||||
|
{ label: 'Size¹', value: '175 × 88 × 77 mm' },
|
||||||
|
{ label: 'Degrees of freedom', value: 'Thumb × 3; Index × 2; Middle × 2' },
|
||||||
|
{ label: 'Transmission', value: '6 motors drive directly; 1 motor drives with gears' },
|
||||||
|
{ label: 'Thumb joint 0', value: '−60° to 60°' },
|
||||||
|
{ label: 'Thumb joint 1', value: '−35° to 60°' },
|
||||||
|
{ label: 'Thumb joint 2', value: '0° to 100°' },
|
||||||
|
{ label: 'Index joint 0', value: '0° to 90°' },
|
||||||
|
{ label: 'Index joint 1', value: '0° to 100°' },
|
||||||
|
{ label: 'Middle joint 0', value: '0° to 90°' },
|
||||||
|
{ label: 'Middle joint 1', value: '0° to 100°' },
|
||||||
|
{ label: 'Fingertip repeat-positioning accuracy', value: '±2 mm' },
|
||||||
|
{ label: 'Working voltage', value: '12 V – 58 V' },
|
||||||
|
{ label: 'Static current', value: '58 V @ 0.2 A' },
|
||||||
|
{ label: 'Maximum current', value: '10 A' },
|
||||||
|
{ label: 'Communication interface', value: 'USB 2.0' },
|
||||||
|
{ label: 'Working temperature range', value: '−20 °C to 60 °C' },
|
||||||
|
{ label: 'Load (palm down, 5 cm round hard object)', value: '500 g max / 400 W peak @ 3 s' },
|
||||||
|
{ label: 'Load (palm left, 5 cm round hard object)', value: '500 g max / 400 W peak @ 3 s' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Sensing Parameters',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Pressure sensor groups', value: '9 (33 sensors total)' },
|
||||||
|
{ label: 'Array resolution', value: '2×2 palm ×3; 2×2 finger pad ×3; 3×1 fingertip ×3' },
|
||||||
|
{ label: 'Perception range', value: '10 g – 2500 g' },
|
||||||
|
{ label: 'Max acceptance (undamaged)', value: '20 kg' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Software Functions',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Communication rate', value: '1000 Hz' },
|
||||||
|
{ label: 'Full packet bytes', value: 'Sender 1234 / Receiver 1270' },
|
||||||
|
{ label: 'Perceptual feedback', value: 'Joint mode, position, velocity, torque, temperature, voltage/current, sensor pressure, sensor temperature, IMU data' },
|
||||||
|
{ label: 'Control feedback', value: 'Joint mode, position, velocity, torque, stiffness coefficient, damping coefficient' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Miniature Brushless Force-Control Joint',
|
||||||
|
variants: ['F-1515-108', 'F-1515-214'],
|
||||||
|
rows: [
|
||||||
|
{ label: 'Weight', value: '45 g', valueAlt: '45 g' },
|
||||||
|
{ label: 'Size', value: '34.8 × 23.1 × 23.3 mm', valueAlt: '34.8 × 23.1 × 23.3 mm' },
|
||||||
|
{ label: 'Reduction ratio', value: '1:108', valueAlt: '1:214' },
|
||||||
|
{ label: 'Maximum torque (ideal)', value: '0.76 N·m', valueAlt: '1.498 N·m' },
|
||||||
|
{ label: 'Max torque (same direction as speed)', value: '0.49 N·m', valueAlt: '0.86 N·m' },
|
||||||
|
{ label: 'Max torque (opposite direction)', value: '1.37 N·m', valueAlt: '3.1 N·m' },
|
||||||
|
{ label: 'Maximum rotational speed', value: '23 rad/s', valueAlt: '11 rad/s' },
|
||||||
|
{ label: 'Maximum line current', value: '4.76 A', valueAlt: '4.76 A' },
|
||||||
|
{ label: 'Communication frequency', value: '1000 Hz', valueAlt: '1000 Hz' },
|
||||||
|
{ label: 'Communication method', value: 'High-speed Unibus', valueAlt: 'High-speed Unibus' },
|
||||||
|
{ label: 'Encoder', value: 'Rotor absolute value encoder', valueAlt: 'Rotor absolute value encoder' },
|
||||||
|
{ label: 'Working temperature', value: '−20 °C to 60 °C', valueAlt: '−20 °C to 60 °C' },
|
||||||
|
{ label: 'Working voltage', value: '12 V – 24 V', valueAlt: '12 V – 24 V' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paramsImage: '/images/accessories/unitree-dex3-1/diagram.png',
|
||||||
|
footnotes: [
|
||||||
|
'Size refers to the dexterous hand in flat state.',
|
||||||
|
'Pressure data refers to force exerted by the dexterous hand when pressed by a vertical 1 cm diameter cylinder. Varies in different application scenarios.',
|
||||||
|
],
|
||||||
|
safetyNotice:
|
||||||
|
'This product is a civilian robot. Users are requested to refrain from making dangerous modifications or using the robot in a hazardous manner. Comply with local laws and regulations and consult Unitree Robotics terms for full policies.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'unitree-dex1-1',
|
id: 'unitree-dex1-1',
|
||||||
@ -115,15 +416,16 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
brand: 'unitree',
|
brand: 'unitree',
|
||||||
brandLabel: 'Unitree',
|
brandLabel: 'Unitree',
|
||||||
name: 'Dex1-1 Embodied AI Gripper',
|
name: 'Dex1-1 Embodied AI Gripper',
|
||||||
tagline: 'Two-finger embodied-AI gripper with optional 1080p camera.',
|
tagline: 'Professional and refined — extra-large stroke, powerful force, ultimate control.',
|
||||||
group: 'dexterous-hand',
|
group: 'dexterous-hand',
|
||||||
description:
|
description:
|
||||||
'A two-finger gripper with 90 mm stroke, 0.1 mm resolution, and 5–120 N adjustable force. Available in Standard and Advanced (with 1080p camera) variants, compatible with G1, H2, R1, or standalone use.',
|
'A two-finger gripper designed specifically for embodied AI applications. Extra-large 90 mm stroke, adjustable 5–120 N gripping force, and 50–375 mm/s adjustable speed deliver versatile pick-and-place performance. Available in Standard and Advanced (with 1080p camera) variants, compatible with G1, H2, R1, or standalone use.',
|
||||||
features: [
|
features: [
|
||||||
'90 mm stroke, 5–120 N adjustable force',
|
'90 mm stroke, 5–120 N adjustable force',
|
||||||
'0.1 mm fingertip resolution',
|
'0.1 mm fingertip position resolution',
|
||||||
'0.24 s full-stroke close',
|
'0.24 s full-stroke close',
|
||||||
'20 kg form-fit grasp payload',
|
'20 kg form-fit grasp payload',
|
||||||
|
'5 kg friction grasp load',
|
||||||
'6 Mbps RS485 communication',
|
'6 Mbps RS485 communication',
|
||||||
'Advanced version includes 1080p camera',
|
'Advanced version includes 1080p camera',
|
||||||
],
|
],
|
||||||
@ -131,6 +433,74 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
accent: GOLD_CHAMPAGNE,
|
accent: GOLD_CHAMPAGNE,
|
||||||
officialUrl: 'https://www.unitree.com/Dex1-1',
|
officialUrl: 'https://www.unitree.com/Dex1-1',
|
||||||
compatibility: ['Unitree G1', 'Unitree H2', 'Unitree R1', 'Standalone'],
|
compatibility: ['Unitree G1', 'Unitree H2', 'Unitree R1', 'Standalone'],
|
||||||
|
highlights: [
|
||||||
|
{ value: '90 mm', label: 'Stroke', sub: 'Extra-large opening range' },
|
||||||
|
{ value: '120 N', label: 'Gripping force', sub: 'Adjustable 5–120 N' },
|
||||||
|
{ value: '20 kg', label: 'Form-fit load', sub: '5 kg friction load' },
|
||||||
|
{ value: '0.24 s', label: 'Full-stroke close', sub: '0.1 mm fingertip resolution' },
|
||||||
|
],
|
||||||
|
featureSections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Design',
|
||||||
|
title: 'Extra-large stroke, powerful force, ultimate control.',
|
||||||
|
body: 'A two-finger gripper designed specifically for embodied AI applications. Extra-large 90 mm stroke, adjustable 5–120 N gripping force, and 50–375 mm/s speed cover laboratory through industrial workflows.',
|
||||||
|
bullets: ['90 mm stroke', '5–120 N gripping force', '0.1 mm fingertip resolution', '80 mm jaw length'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Industry applications',
|
||||||
|
title: 'Built for real-world deployment.',
|
||||||
|
body: 'From research labs to industrial floors, Dex1-1 adapts to a wide range of pick-and-place scenarios.',
|
||||||
|
bullets: ['Scientific research and education', 'Courier sorting', 'Industrial automation', 'New retail'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
specGroups: [
|
||||||
|
{
|
||||||
|
title: 'Gripper Parameters',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Model', value: 'Dex1-1' },
|
||||||
|
{ label: 'Dimensions¹', value: '143 × 78 × 67 mm' },
|
||||||
|
{ label: 'Weight', value: '550 g' },
|
||||||
|
{ label: 'Stroke', value: '90 mm' },
|
||||||
|
{ label: 'Gripping force² (adjustable)', value: '5 – 120 N' },
|
||||||
|
{ label: 'Speed² (adjustable)', value: '50 – 375 mm/s' },
|
||||||
|
{ label: 'Fingertip position resolution', value: '0.1 mm' },
|
||||||
|
{ label: 'Grasp load³ (form-fitting)', value: '20 kg' },
|
||||||
|
{ label: 'Grasp load⁴ (friction)', value: '5 kg' },
|
||||||
|
{ label: 'Full-stroke shortest closing time²', value: '0.24 s' },
|
||||||
|
{ label: 'Jaw length', value: '80 mm' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Motor Parameters',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Communication protocol', value: 'High-speed 485' },
|
||||||
|
{ label: 'Communication baud rate', value: '6 Mbps' },
|
||||||
|
{ label: 'Communication frequency', value: '1000 Hz' },
|
||||||
|
{ label: 'Operating voltage', value: '24 V – 64 V (24 / 48 VDC recommended)' },
|
||||||
|
{ label: 'Static current', value: '0.03 A' },
|
||||||
|
{ label: 'Peak line current', value: '3.5 A' },
|
||||||
|
{ label: 'Motor peak torque', value: '5 N·m' },
|
||||||
|
{ label: 'Motor max speed', value: '27 rad/s (with 64 V power supply)' },
|
||||||
|
{ label: 'Recommended operating environment', value: '−20 °C to 60 °C' },
|
||||||
|
{ label: 'Motor reduction ratio', value: '1:25' },
|
||||||
|
{ label: 'Encoder resolution', value: '15 bit' },
|
||||||
|
{ label: 'Encoder type', value: 'Absolute encoder' },
|
||||||
|
{ label: 'Motor status feedback', value: 'Angle, angular velocity, torque, temperature' },
|
||||||
|
{ label: 'Motor control instruction', value: 'Angle, angular velocity, torque, stiffness, damping' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paramsImage: '/images/accessories/unitree-dex1-1/diagram.png',
|
||||||
|
footnotes: [
|
||||||
|
'The gripper width is 78 mm when half-open, 120 mm when fully open, and 124.5 mm when fully closed (with side rails extended).',
|
||||||
|
'Listed values for speed, gripping force, and full-stroke closing time are measured under varying conditions (24 V – 60 V) by adjusting motor stiffness, damping, and torque. Values may vary depending on the application.',
|
||||||
|
'Grasp load (form-fitting) refers to the maximum horizontal load the gripper can hold without losing its grip. Varies by application.',
|
||||||
|
'Grasp load (friction) refers to the maximum vertical load the gripper can hold without slipping, measured with a square steel block. Varies by application.',
|
||||||
|
'All parameters above may vary in different application scenarios or model configurations.',
|
||||||
|
'Product appearance is subject to change. Refer to the final product.',
|
||||||
|
],
|
||||||
|
safetyNotice:
|
||||||
|
'This product is a civilian robot. Users are requested to refrain from making dangerous modifications or using the robot in a hazardous manner. Comply with local laws and regulations and consult Unitree Robotics terms for full policies.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ============ UNITREE ROBOTIC ARMS ============
|
// ============ UNITREE ROBOTIC ARMS ============
|
||||||
@ -140,10 +510,10 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
brand: 'unitree',
|
brand: 'unitree',
|
||||||
brandLabel: 'Unitree',
|
brandLabel: 'Unitree',
|
||||||
name: 'Z1 6-Axis Manipulator',
|
name: 'Z1 6-Axis Manipulator',
|
||||||
tagline: 'Onboard 6-axis manipulator for mobile robotics platforms.',
|
tagline: 'Perfect synergy — onboard 6-axis manipulator for mobile platforms.',
|
||||||
group: 'robotic-arm',
|
group: 'robotic-arm',
|
||||||
description:
|
description:
|
||||||
'Z1 Air (4.3 kg) and Z1 Pro (4.5 kg) 6-axis robotic arms for mobile robots in logistics, retail, and research. 740 mm reach, 2–3 kg payload, ~0.1 mm repeatability. Designed for AlienGo and B1 mounting.',
|
'Z1 Air (4.3 kg) and Z1 Pro (4.5 kg) 6-axis robotic arms designed for diverse application scenarios — e-commerce logistics, new consumption, daily life, and research. 740 mm reach, 2–3 kg payload, and ~0.1 mm repeatability. Pairs natively with AlienGo or B1 quadruped chassis and supports joint force control with collision protection.',
|
||||||
features: [
|
features: [
|
||||||
'6-axis, 740 mm reach',
|
'6-axis, 740 mm reach',
|
||||||
'4.3 kg (Air) / 4.5 kg (Pro)',
|
'4.3 kg (Air) / 4.5 kg (Pro)',
|
||||||
@ -151,11 +521,100 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
'~0.1 mm repeatability',
|
'~0.1 mm repeatability',
|
||||||
'Joint force control + collision detection',
|
'Joint force control + collision detection',
|
||||||
'Position + force control modes',
|
'Position + force control modes',
|
||||||
|
'Harmonic reducer joint motors',
|
||||||
|
'Ethernet interface, Ubuntu OS',
|
||||||
],
|
],
|
||||||
image: '/images/accessories/unitree-z1.png',
|
image: '/images/accessories/unitree-z1.png',
|
||||||
accent: GOLD_BRAND,
|
accent: GOLD_BRAND,
|
||||||
officialUrl: 'https://www.unitree.com/z1',
|
officialUrl: 'https://www.unitree.com/z1',
|
||||||
compatibility: ['Unitree AlienGo', 'Unitree B1', 'Mobile platforms'],
|
compatibility: ['Unitree AlienGo', 'Unitree B1', 'Mobile platforms'],
|
||||||
|
highlights: [
|
||||||
|
{ value: '6', label: 'Axis DOF', sub: 'Force-controllable' },
|
||||||
|
{ value: '740 mm', label: 'Reach', sub: 'Z1 Air / Z1 Pro' },
|
||||||
|
{ value: '≥3 kg', label: 'Payload (Pro)', sub: '2 kg on Air' },
|
||||||
|
{ value: '~0.1 mm', label: 'Repeatability', sub: 'Position + force control' },
|
||||||
|
],
|
||||||
|
featureSections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Characteristics',
|
||||||
|
title: 'Designed for mobile manipulation.',
|
||||||
|
body: 'Compact, lightweight, and dexterous, the Z1 delivers sufficient payload and accuracy for mobile-robot pairings. Supports joint force control with built-in collision protection.',
|
||||||
|
bullets: [
|
||||||
|
'Compact and lightweight',
|
||||||
|
'Dexterous and flexible',
|
||||||
|
'Sufficient payload',
|
||||||
|
'Sufficient accuracy',
|
||||||
|
'Joint force control',
|
||||||
|
'Collision protection',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Open programming + extensible interface',
|
||||||
|
title: 'Open SDK, swappable end-effectors.',
|
||||||
|
body: 'The manipulator control program and control interface are opened progressively, and different actuators can be quickly replaced at the end of the manipulator for varied workflows.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Application',
|
||||||
|
title: 'Mobile manipulation across sectors.',
|
||||||
|
body: 'Onboard manipulator for various mobile robots — e-commerce logistics, new consumption, daily-life automation, and research labs. Native AlienGo / B1 mount.',
|
||||||
|
bullets: ['Pairs with AlienGo or B1 quadruped', 'Onboard manipulator for mobile robots'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
specGroups: [
|
||||||
|
{
|
||||||
|
title: 'Joint Motor',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Backlash', value: '~6 arcmin' },
|
||||||
|
{ label: 'Communication method', value: 'RS485' },
|
||||||
|
{ label: 'Maximum torque', value: '33 N·m' },
|
||||||
|
{ label: 'Encoder resolution', value: '15 bit' },
|
||||||
|
{ label: 'Weight', value: '405 g' },
|
||||||
|
{ label: 'Force control accuracy', value: '~0.2 N·m' },
|
||||||
|
{ label: 'Size', value: 'Φ65 × 52 mm' },
|
||||||
|
{ label: 'Control frequency', value: '1 kHz' },
|
||||||
|
{ label: 'Reducer', value: 'Harmonic reducer' },
|
||||||
|
{ label: 'Bearing', value: 'Industrial-grade cross roller' },
|
||||||
|
{ label: 'Reduction ratio', value: '60+' },
|
||||||
|
{ label: 'Motor sensing feedback', value: 'Torque, angle, angular velocity' },
|
||||||
|
{ label: 'Voltage', value: '24 V (recommended)' },
|
||||||
|
{ label: 'Motor control command', value: 'Torque, angle, angular velocity, stiffness, damping' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Parameters',
|
||||||
|
variants: ['Z1 Air', 'Z1 Pro'],
|
||||||
|
rows: [
|
||||||
|
{ label: 'DOF', value: '6 Axis', valueAlt: '6 Axis' },
|
||||||
|
{ label: 'Weight', value: '4.3 kg', valueAlt: '4.5 kg' },
|
||||||
|
{ label: 'Payload', value: '2 kg', valueAlt: '≥3 kg' },
|
||||||
|
{ label: 'Reach', value: '740 mm', valueAlt: '740 mm' },
|
||||||
|
{ label: 'Repeatability¹', value: '~0.1 mm', valueAlt: '~0.1 mm' },
|
||||||
|
{ label: 'Power supply', value: '24 V, >20 A', valueAlt: '24 V, >20 A' },
|
||||||
|
{ label: 'Interface', value: 'Ethernet', valueAlt: 'Ethernet' },
|
||||||
|
{ label: 'Operating system', value: 'Ubuntu', valueAlt: 'Ubuntu' },
|
||||||
|
{ label: 'Power', value: 'MAX 500 W', valueAlt: 'MAX 500 W' },
|
||||||
|
{ label: 'Force feedback + collision detection', value: 'Provided', valueAlt: 'Provided' },
|
||||||
|
{ label: 'Control interface', value: 'Position + Force Control', valueAlt: 'Position + Force Control' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Joint Range & Speed',
|
||||||
|
rows: [
|
||||||
|
{ label: 'J1', value: '±150° · 180°/s' },
|
||||||
|
{ label: 'J2', value: '0° to 180° · 180°/s' },
|
||||||
|
{ label: 'J3', value: '−165° to 0° · 180°/s' },
|
||||||
|
{ label: 'J4', value: '±80° · 180°/s' },
|
||||||
|
{ label: 'J5', value: '±85° · 180°/s' },
|
||||||
|
{ label: 'J6', value: '±160° · 180°/s' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
footnotes: [
|
||||||
|
'Accuracy depends on actual test conditions; standards for manipulator testing vary, and accuracy varies under different test conditions.',
|
||||||
|
'Because each joint uses a relatively low reduction ratio, whole-machine position control stiffness is low. If the control mode is not optimized, there may be position-control error and shaking during movement.',
|
||||||
|
],
|
||||||
|
safetyNotice:
|
||||||
|
'This product is a civilian robot. Users are requested to refrain from making dangerous modifications or using the robot in a hazardous manner. Comply with local laws and regulations and consult Unitree Robotics terms for full policies.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'unitree-d1-t',
|
id: 'unitree-d1-t',
|
||||||
@ -163,22 +622,78 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
brand: 'unitree',
|
brand: 'unitree',
|
||||||
brandLabel: 'Unitree',
|
brandLabel: 'Unitree',
|
||||||
name: 'D1-T Teleoperation Arm',
|
name: 'D1-T Teleoperation Arm',
|
||||||
tagline: 'Teleoperation robot arm for embodied-AI data collection.',
|
tagline: 'Teleoperation robot arm — empowers embodied intelligence research.',
|
||||||
group: 'robotic-arm',
|
group: 'robotic-arm',
|
||||||
description:
|
description:
|
||||||
'Dual-arm or quad-arm teleoperation kit with 2.37 kg ultra-light aluminum design. Bus servos featuring position/velocity/force control for embodied-AI data collection. 7-DOF (6-axis + gripper), 670 mm reach, 500 g payload.',
|
'Dual-arm (<$8,500) or quad-arm (<$16,000) teleoperation kit with 2.37 kg aluminum-alloy body and 670 mm reach. Ultra-precision force-controlled digital servo motors with position, velocity, and force control modes. Designed for embodied-AI data acquisition and continuous learning workflows.',
|
||||||
features: [
|
features: [
|
||||||
'7-DOF arm (6-axis + gripper)',
|
'7-DOF arm (6 axis + gripper)',
|
||||||
'2.37 kg, 670 mm reach, 500 g payload',
|
'2.37 kg aluminum alloy, 670 mm reach',
|
||||||
|
'500 g payload',
|
||||||
'Ultra-precision force-controlled servos',
|
'Ultra-precision force-controlled servos',
|
||||||
'Dual-arm / Quad-arm configurations',
|
'Dual-arm / Quad-arm kit options',
|
||||||
'Camera & mobile-chassis ready',
|
'Camera + mobile-chassis ready',
|
||||||
'24 V (2.5 A standard / 5 A max)',
|
'24 V @ 2.5 A standard / 5 A max',
|
||||||
|
'RJ45 + Type-C debug interface',
|
||||||
],
|
],
|
||||||
image: '/images/accessories/unitree-d1-t.png',
|
image: '/images/accessories/unitree-d1-t.jpg',
|
||||||
accent: GOLD_BRONZE,
|
accent: GOLD_BRONZE,
|
||||||
officialUrl: 'https://www.unitree.com/D1-T',
|
officialUrl: 'https://www.unitree.com/D1-T',
|
||||||
compatibility: ['Embodied AI research', 'Data collection'],
|
compatibility: ['Embodied AI research', 'Data collection'],
|
||||||
|
highlights: [
|
||||||
|
{ value: '6 + 1', label: 'Degrees of freedom', sub: '6 axis + 1 gripper' },
|
||||||
|
{ value: '2.37 kg', label: 'Weight', sub: 'Aluminum alloy body' },
|
||||||
|
{ value: '670 mm', label: 'Reach', sub: 'With gripper' },
|
||||||
|
{ value: '500 g', label: 'Payload', sub: '24 V @ 2.5 A (MAX 5 A)' },
|
||||||
|
],
|
||||||
|
featureSections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'Servo motor',
|
||||||
|
title: 'Ultra-precision force-controlled digital servo motor.',
|
||||||
|
body: 'Position control, velocity control, and force control in one motor — suitable for application development and learning tasks across diverse scenarios. Low-level and high-level interfaces expose direct motor control and advanced functions.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Lightweight body',
|
||||||
|
title: 'Ultra-lightweight aluminum-alloy robot arm.',
|
||||||
|
body: 'Aluminum-alloy construction holds the full arm at 2.37 kg with a 670 mm arm length, keeping the platform mobile-ready and easy to transport.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Degrees of freedom',
|
||||||
|
title: 'A “true” six-axis arm with gripper.',
|
||||||
|
body: 'Six joints plus a gripper add an extra degree of freedom for greater spatial flexibility — covering a wider envelope of motion than typical research arms.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Hardware compatibility',
|
||||||
|
title: 'Ultra-flexible hardware integration.',
|
||||||
|
body: 'Connects to external devices — cameras, mobile robot chassis, sensors — for diverse application setups across embodied-AI workflows.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
specGroups: [
|
||||||
|
{
|
||||||
|
title: 'Parameters',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Model', value: 'D1' },
|
||||||
|
{ label: 'Weight', value: '~2.37 kg' },
|
||||||
|
{ label: 'DOF', value: '6 Axis + 1 Gripper' },
|
||||||
|
{ label: 'Payload', value: '500 g' },
|
||||||
|
{ label: 'Reach (no gripper)', value: '550 mm' },
|
||||||
|
{ label: 'Reach (with gripper)', value: '670 mm' },
|
||||||
|
{ label: 'Power supply', value: '24 V @ 2.5 A (MAX 5 A)' },
|
||||||
|
{ label: 'Interface', value: 'DC5.5-2.1 power + RJ45 communication + Type-C debug' },
|
||||||
|
{ label: 'Motor type', value: 'Bus servo' },
|
||||||
|
{ label: 'Power', value: '60 W' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Pricing',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Dual-Arm Teleoperation Kit (Standard)', value: '< $8,500 (tax + freight excluded)' },
|
||||||
|
{ label: 'Quad-Arm Teleoperation Kit (Full)', value: '< $16,000 (tax + freight excluded)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
safetyNotice:
|
||||||
|
'This product is a civilian robot. Users are requested to refrain from making dangerous modifications or using the robot in a hazardous manner. Comply with local laws and regulations and consult Unitree Robotics terms for full policies.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ============ UNITREE PERCEPTION & SENSORS ============
|
// ============ UNITREE PERCEPTION & SENSORS ============
|
||||||
@ -188,22 +703,120 @@ export const ACCESSORIES: Accessory[] = [
|
|||||||
brand: 'unitree',
|
brand: 'unitree',
|
||||||
brandLabel: 'Unitree',
|
brandLabel: 'Unitree',
|
||||||
name: '4D LiDAR L2',
|
name: '4D LiDAR L2',
|
||||||
tagline: '4D LiDAR with built-in IMU and open-source SLAM (POINT-LIO).',
|
tagline: 'All new L2 — Unitree 4D LiDAR with built-in IMU and open-source SLAM.',
|
||||||
group: 'sensor',
|
group: 'sensor',
|
||||||
description:
|
description:
|
||||||
'A 4D LiDAR sensor with built-in 6-axis IMU and open POINT-LIO SLAM stack. 360°×96° ultra-wide field of view at 64,000 pts/s with 2 cm accuracy. 230 g compact form, 30 m range at 90% reflectivity.',
|
'A $419 4D LiDAR sensor with built-in 6-axis IMU and the open POINT-LIO SLAM stack. 360° × 96° ultra-wide field of view at 64,000 pts/s effective frequency, 30 m range @ 90% reflectivity, 4.5 mm distance resolution, and 2 cm measurement accuracy. 230 g compact form. Robust under 100 klux outdoor sunlight.',
|
||||||
features: [
|
features: [
|
||||||
'360°×96° ultra-wide FOV',
|
'360° × 96° ultra-wide FOV (+7%)',
|
||||||
'64,000 pts/s, 2 cm accuracy',
|
'64,000 pts/s effective frequency (+200%)',
|
||||||
'30 m @ 90% reflectivity',
|
'4.5 mm distance resolution (+78%)',
|
||||||
'Built-in 6-axis IMU',
|
'30 m range @ 90% reflectivity',
|
||||||
'230 g compact form',
|
'2 cm measurement accuracy',
|
||||||
'Open POINT-LIO SLAM stack',
|
'230 g, 75 × 75 × 65 mm',
|
||||||
|
'Built-in 3-axis acc + 3-axis gyro IMU',
|
||||||
|
'ENET UDP / TTL UART communication',
|
||||||
|
'Open POINT-LIO SLAM solution',
|
||||||
],
|
],
|
||||||
image: '/images/accessories/unitree-l2.png',
|
image: '/images/accessories/unitree-l2.jpg',
|
||||||
accent: GOLD_CHAMPAGNE,
|
accent: GOLD_CHAMPAGNE,
|
||||||
officialUrl: 'https://www.unitree.com/L2',
|
officialUrl: 'https://www.unitree.com/L2',
|
||||||
compatibility: ['Unitree Go2', 'Unitree quadrupeds', 'Third-party robots'],
|
compatibility: ['Unitree Go2', 'Unitree quadrupeds', 'Third-party robots'],
|
||||||
|
highlights: [
|
||||||
|
{ value: '360°×96°', label: 'Ultra-wide FOV', sub: '+7% scanning' },
|
||||||
|
{ value: '64,000', label: 'Points/s', sub: 'Effective frequency · +200%' },
|
||||||
|
{ value: '30 m', label: 'Scan range', sub: '@ 90% reflectivity' },
|
||||||
|
{ value: '4.5 mm', label: 'Distance resolution', sub: '+78% improvement' },
|
||||||
|
],
|
||||||
|
featureSections: [
|
||||||
|
{
|
||||||
|
eyebrow: 'SLAM',
|
||||||
|
title: 'Complete open-source SLAM solution.',
|
||||||
|
body: 'Use only L2 and its built-in IMU to map with the POINT-LIO algorithm — no other positioning sensors required. Open-source release on github.com/unitreerobotics/point_lio_unilidar.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Indoor',
|
||||||
|
title: 'Whole-house dynamic scanning.',
|
||||||
|
body: 'L2 quickly and accurately captures 3D structural information of an entire house — supports robot positioning and autonomous navigation, plus extended functions like whole-house cleaning and organization.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Outdoor',
|
||||||
|
title: 'Real-scene dynamic scanning under strong sun.',
|
||||||
|
body: 'Resists indoor ambient-light and outdoor strong-light interference. Achieves stable ranging and high-precision mapping under outdoor conditions of 100 klux.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eyebrow: 'Scanning pattern',
|
||||||
|
title: 'Non-repetitive omnidirectional scanning.',
|
||||||
|
body: 'High-precision, high-density point-cloud data via omnidirectional ultra-wide-angle non-repetitive scanning — photograph-level scanning detail.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
specGroups: [
|
||||||
|
{
|
||||||
|
title: 'Headline Parameters',
|
||||||
|
rows: [
|
||||||
|
{ label: 'FOV', value: '360° × 96°' },
|
||||||
|
{ label: 'Near blind spot', value: '0.05 m' },
|
||||||
|
{ label: 'Scan range', value: '30 m @ 90% reflectivity' },
|
||||||
|
{ label: 'Effective frequency', value: '64,000 points/s' },
|
||||||
|
{ label: 'Measurement accuracy', value: '2 cm' },
|
||||||
|
{ label: 'Weight', value: '230 g' },
|
||||||
|
{ label: 'Communication interface', value: 'ENET UDP / TTL UART' },
|
||||||
|
{ label: 'Size', value: '75 × 75 × 65 mm' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Comparison vs other sensors',
|
||||||
|
variants: ['Unitree L2', 'Depth Camera', 'Wide-Angle 3D Radar'],
|
||||||
|
rows: [
|
||||||
|
{ label: 'FOV', value: '360° × 96°', valueAlt: '86° × 57° (±3°)' },
|
||||||
|
{ label: 'Non-repetitive scanning', value: '✓', valueAlt: '—' },
|
||||||
|
{ label: 'Scanning method', value: 'Non-contact brushless rotating mirror', valueAlt: 'Global shutter' },
|
||||||
|
{ label: 'Scan range', value: '30 m', valueAlt: '4 – 5 m' },
|
||||||
|
{ label: 'Near blind area', value: '0.05 m', valueAlt: '0.1 m' },
|
||||||
|
{ label: 'IMU', value: '3-axis acc + 3-axis gyro', valueAlt: '—' },
|
||||||
|
{ label: 'Power', value: '10 W', valueAlt: '5 W' },
|
||||||
|
{ label: 'Weight', value: '230 g', valueAlt: '~100 g' },
|
||||||
|
{ label: 'Technology', value: 'Laser TOF', valueAlt: 'Infrared binocular' },
|
||||||
|
{ label: 'Price', value: '$419', valueAlt: '$359' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Family Comparison',
|
||||||
|
variants: ['L2', 'L1 PM', 'L1 RM'],
|
||||||
|
rows: [
|
||||||
|
{ label: 'Price', value: '$419', valueAlt: '$249' },
|
||||||
|
{ label: 'Scan range (90% reflectivity)', value: '30 m', valueAlt: '20 m' },
|
||||||
|
{ label: 'Scan range (10% reflectivity)', value: '15 m', valueAlt: '10 m' },
|
||||||
|
{ label: 'FOV', value: '360° × 96°', valueAlt: '360° × 90°' },
|
||||||
|
{ label: 'Sampling frequency', value: '128,000 pts/s', valueAlt: '43,200 pts/s' },
|
||||||
|
{ label: 'Effective frequency', value: '64,000 pts/s', valueAlt: '21,600 pts/s' },
|
||||||
|
{ label: 'Communication', value: 'ENET UDP / TTL UART', valueAlt: 'TTL UART' },
|
||||||
|
{ label: '4D data', value: '3D position + 1D grayscale (2D mode supported)', valueAlt: '3D position + 1D grayscale' },
|
||||||
|
{ label: 'Circumferential scan frequency', value: '5.55 Hz (adjustable)', valueAlt: '11 Hz' },
|
||||||
|
{ label: 'Vertical scan frequency', value: '216 Hz', valueAlt: '180 Hz' },
|
||||||
|
{ label: 'Distance resolution', value: '4.5 mm', valueAlt: '8 mm' },
|
||||||
|
{ label: 'Measurement accuracy', value: '≤ 2.0 cm', valueAlt: '± 2.0 cm' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Common Specs (all models)',
|
||||||
|
rows: [
|
||||||
|
{ label: 'Near blind area', value: '0.05 m' },
|
||||||
|
{ label: 'IMU', value: '3-axis acceleration + 3-axis gyroscope' },
|
||||||
|
{ label: 'Eye safety level', value: 'Class 1 (IEC 60825-1:2014)' },
|
||||||
|
{ label: 'Anti-glare capability', value: '> 100 klux' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
footnotes: [
|
||||||
|
'L2 supports negative-angle mode where FOV extends from 360° × 90° to 360° × 96°. The extra 6° has slightly shorter maximum measurement distance.',
|
||||||
|
'Reflectance values are typical; actual values depend on environment and target characteristics.',
|
||||||
|
'To ensure effective detection of objects with different reflectance, point-cloud accuracy may decrease slightly at certain positions. Farthest range is directly above the L2.',
|
||||||
|
'Stable and peak power vary with environment. At −10 °C to 30 °C, L2 automatically enters self-heating mode; point cloud data is generated only when temperature meets requirements, and peak power can reach 13 W.',
|
||||||
|
'Parameters above may vary across business scenarios and LiDAR configurations.',
|
||||||
|
],
|
||||||
|
safetyNotice:
|
||||||
|
'This product is a civilian robot. Users are requested to refrain from making dangerous modifications or using the robot in a hazardous manner. Comply with local laws and regulations and consult Unitree Robotics terms for full policies.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ============ PUDU DISPATCH ============
|
// ============ PUDU DISPATCH ============
|
||||||
|
|||||||
10
src/data/bu-sunaidah-posts.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"url": "https://www.instagram.com/reel/DX6pDm9Mkwe/",
|
||||||
|
"caption": "Make It in the Emirates 2026 — Abu Dhabi appearance."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.instagram.com/reel/DYg5KQsC8pW/",
|
||||||
|
"caption": "Abu Dhabi Global Sustainable Security Summit activation."
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -29,9 +29,21 @@ export const BU_SUNAIDAH_URL = `https://www.instagram.com/${BU_SUNAIDAH_HANDLE}`
|
|||||||
*/
|
*/
|
||||||
export const BU_SUNAIDAH_PORTRAIT: { src: string; alt: string } | null = null;
|
export const BU_SUNAIDAH_PORTRAIT: { src: string; alt: string } | null = null;
|
||||||
|
|
||||||
// TODO: populate with curated post shortcodes from @bu.sunaidah.
|
// Curated reel shortcodes from @bu.sunaidah, sourced from publicly indexed
|
||||||
// Each entry renders as a 16:20 Instagram embed iframe.
|
// Instagram URLs. Each entry renders as an official Instagram embed iframe.
|
||||||
export const BU_SUNAIDAH_POSTS: InstagramPost[] = [];
|
// Captions are short factual descriptions of the event — not lifted from IG.
|
||||||
|
export const BU_SUNAIDAH_POSTS: InstagramPost[] = [
|
||||||
|
{
|
||||||
|
shortcode: 'DX6pDm9Mkwe',
|
||||||
|
kind: 'reel',
|
||||||
|
caption: 'Make It in the Emirates 2026 — Abu Dhabi appearance.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcode: 'DYg5KQsC8pW',
|
||||||
|
kind: 'reel',
|
||||||
|
caption: 'Abu Dhabi Global Sustainable Security Summit activation.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bu Sunaidah video reel / showreel.
|
* Bu Sunaidah video reel / showreel.
|
||||||
|
|||||||
105
src/lib/bu-sunaidah-fetch.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Fetch the latest Bu Sunaidah Instagram posts — WITHOUT the Graph API.
|
||||||
|
*
|
||||||
|
* Honest reality:
|
||||||
|
* Instagram blocks unauthenticated scraping. There is no reliable, public
|
||||||
|
* endpoint that returns a profile's latest media without an access token
|
||||||
|
* or a logged-in session cookie. We attempt a best-effort HTML pull and
|
||||||
|
* gracefully fall back to a locally-edited JSON file.
|
||||||
|
*
|
||||||
|
* Resolution order (first hit wins):
|
||||||
|
* 1. Best-effort HTML scrape of https://www.instagram.com/bu.sunaidah/
|
||||||
|
* — usually returns nothing because IG serves a login wall, but try.
|
||||||
|
* 2. `src/data/bu-sunaidah-posts.json` — the editable source of truth.
|
||||||
|
* Update this file (or replace via a build step) to refresh the page.
|
||||||
|
* 3. The curated `BU_SUNAIDAH_POSTS` array (compiled-in last resort).
|
||||||
|
*
|
||||||
|
* To refresh the page: edit `src/data/bu-sunaidah-posts.json` with the
|
||||||
|
* latest 9 URLs from https://www.instagram.com/bu.sunaidah/ and rebuild
|
||||||
|
* (or rely on the page's ISR revalidate window).
|
||||||
|
*/
|
||||||
|
import { BU_SUNAIDAH_POSTS, type InstagramPost } from '@/data/bu-sunaidah';
|
||||||
|
import postsFile from '@/data/bu-sunaidah-posts.json' assert { type: 'json' };
|
||||||
|
|
||||||
|
const PROFILE_URL = 'https://www.instagram.com/bu.sunaidah/';
|
||||||
|
const BROWSER_UA =
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36';
|
||||||
|
|
||||||
|
type JsonEntry = {
|
||||||
|
url?: string;
|
||||||
|
shortcode?: string;
|
||||||
|
kind?: 'p' | 'reel' | 'tv';
|
||||||
|
caption?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function shortcodeFromUrl(raw: string): { shortcode: string; kind: 'p' | 'reel' | 'tv' } | null {
|
||||||
|
const match = raw.match(/instagram\.com\/(p|reel|tv)\/([^/?#"\\]+)/i);
|
||||||
|
if (!match) return null;
|
||||||
|
return { shortcode: match[2], kind: match[1].toLowerCase() as 'p' | 'reel' | 'tv' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueByShortcode(list: InstagramPost[]): InstagramPost[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: InstagramPost[] = [];
|
||||||
|
for (const p of list) {
|
||||||
|
if (seen.has(p.shortcode)) continue;
|
||||||
|
seen.add(p.shortcode);
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromJsonFile(): InstagramPost[] {
|
||||||
|
const entries = (postsFile as JsonEntry[]) ?? [];
|
||||||
|
const out: InstagramPost[] = [];
|
||||||
|
for (const e of entries) {
|
||||||
|
if (e.shortcode) {
|
||||||
|
out.push({ shortcode: e.shortcode, kind: e.kind ?? 'p', caption: e.caption });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (e.url) {
|
||||||
|
const parsed = shortcodeFromUrl(e.url);
|
||||||
|
if (parsed) out.push({ ...parsed, caption: e.caption });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueByShortcode(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrapeProfile(limit: number): Promise<InstagramPost[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(PROFILE_URL, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': BROWSER_UA,
|
||||||
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
},
|
||||||
|
next: { revalidate: 1800 },
|
||||||
|
});
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
// Pull every /p/<code>/ and /reel/<code>/ reference out of the HTML.
|
||||||
|
const found = new Map<string, InstagramPost>();
|
||||||
|
const re = /\/(p|reel|tv)\/([A-Za-z0-9_-]{5,})\//g;
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = re.exec(html)) !== null) {
|
||||||
|
const shortcode = m[2];
|
||||||
|
if (found.has(shortcode)) continue;
|
||||||
|
found.set(shortcode, { shortcode, kind: m[1].toLowerCase() as 'p' | 'reel' | 'tv' });
|
||||||
|
if (found.size >= limit) break;
|
||||||
|
}
|
||||||
|
return Array.from(found.values());
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestBuSunaidahPosts(limit = 9): Promise<InstagramPost[]> {
|
||||||
|
const scraped = await scrapeProfile(limit);
|
||||||
|
if (scraped.length > 0) return scraped.slice(0, limit);
|
||||||
|
|
||||||
|
const fromFile = fromJsonFile();
|
||||||
|
if (fromFile.length > 0) return fromFile.slice(0, limit);
|
||||||
|
|
||||||
|
return BU_SUNAIDAH_POSTS.slice(0, limit);
|
||||||
|
}
|
||||||