Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -727,3 +727,55 @@ h4{font-size:18px;line-height:1.3}
.faq-cta{padding:24px}
.vn-count{margin-left:0;width:100%}
}

/* ================================================================
Vista Medios (press-)
================================================================ */
.press-feature{display:grid;grid-template-columns:1.3fr 1fr;border:1px solid var(--line);border-radius:var(--r-xl);overflow:hidden;background:var(--bg);margin-top:8px}
.press-feature-copy{padding:40px}
.press-feature-copy .ey{font-family:var(--mono);font-size:11px;letter-spacing:.07em;color:var(--brand);font-weight:500}
.press-feature-copy h3{font-size:28px;margin-top:12px;letter-spacing:-.02em;color:var(--ink)}
.press-feature-copy p{color:var(--ink-2);font-size:15.5px;line-height:1.6;margin-top:14px;max-width:52ch}
.press-feature-meta{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px;align-items:center}
.press-quote{background:var(--ink-bg);color:var(--ink-text);padding:40px;display:flex;flex-direction:column;justify-content:center;gap:18px}
.press-quote blockquote{font-size:18px;line-height:1.5;letter-spacing:-.01em;color:var(--ink-text)}
.press-quote cite{font-style:normal;font-family:var(--mono);font-size:11.5px;letter-spacing:.04em;color:var(--ink-text-2);line-height:1.6;display:block}
.press-quote cite b{display:block;color:var(--ink-text);font-weight:500;font-family:var(--sans);font-size:13.5px;letter-spacing:0;margin-bottom:3px}

.press-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:8px}
.press-card{border:1px solid var(--line);border-radius:var(--r-lg);background:var(--bg);padding:22px;display:flex;flex-direction:column;gap:14px;transition:border-color .15s,transform .12s,box-shadow .15s}
.press-card:hover{border-color:var(--line-2);transform:translateY(-2px);box-shadow:0 12px 28px -18px rgba(25,24,23,.2)}
.press-card.is-hidden{display:none}
.press-outlet{display:flex;align-items:center;gap:11px}
.press-mark{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:-.03em;flex-shrink:0;overflow:hidden}
.press-outlet>span{min-width:0}
.press-outlet .press-outlet-name,.press-outlet .press-outlet-date{display:block}
.press-outlet-name{font-size:13.5px;font-weight:500;color:var(--ink);line-height:1.25}
.press-outlet-date{font-family:var(--mono);font-size:10.5px;color:var(--ink-4);margin-top:2px}
.press-card h3{font-size:16.5px;line-height:1.32;color:var(--ink);letter-spacing:-.01em}
.press-card p{font-size:13.5px;color:var(--ink-2);line-height:1.55;flex:1}
.press-card-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;padding-top:13px;border-top:1px dashed var(--line)}
.press-tags{display:flex;gap:6px;flex-wrap:wrap}
.press-link{font-family:var(--mono);font-size:11.5px;color:var(--brand-on);display:inline-flex;align-items:center;gap:6px;font-weight:500}
.press-card:hover .press-link{text-decoration:underline}
.press-link svg{width:11px;height:11px}

/* tipos de medio, reusando la paleta de la marca */
.tag.prensa{background:var(--models-soft);color:var(--models-deep);border-color:#c6d9ff}
.tag.tv{background:var(--plugins-soft);color:var(--plugins-deep);border-color:#f2d7f4}
.tag.radio{background:var(--datasets-soft);color:var(--datasets-deep);border-color:#f6cdb2}
.tag.institucional{background:var(--generative-soft);color:var(--generative-deep);border-color:#b7e9d2}
.press-mark.prensa{background:var(--models-soft);color:var(--models-deep)}
.press-mark.tv{background:var(--plugins-soft);color:var(--plugins-deep)}
.press-mark.radio{background:var(--datasets-soft);color:var(--datasets-deep)}
.press-mark.institucional{background:var(--generative-soft);color:var(--generative-deep)}

@media (max-width:980px){
.press-feature{grid-template-columns:1fr}
.press-grid{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:640px){
.press-grid{grid-template-columns:1fr}
.press-feature-copy,.press-quote{padding:28px}
.press-feature-copy h3{font-size:23px}
}
10 changes: 10 additions & 0 deletions app/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,15 @@ import footerDE from "../public/locales/de/footer.json"
import footerZH from "../public/locales/zh/footer.json"

import faqEN from "../public/locales/en/faq.json"
import newsEN from "../public/locales/en/news.json"
import faqES from "../public/locales/es/faq.json"
import newsES from "../public/locales/es/news.json"
import faqPT from "../public/locales/pt/faq.json"
import newsPT from "../public/locales/pt/news.json"
import faqDE from "../public/locales/de/faq.json"
import newsDE from "../public/locales/de/news.json"
import faqZH from "../public/locales/zh/faq.json"
import newsZH from "../public/locales/zh/news.json"

i18n
.use(initReactI18next)
Expand All @@ -77,6 +82,7 @@ i18n
about: aboutES,
footer: footerES,
faq: faqES,
news: newsES,
},
en: {
navbar: navbarEN,
Expand All @@ -89,6 +95,7 @@ i18n
about: aboutEN,
footer: footerEN,
faq: faqEN,
news: newsEN,
},
pt: {
navbar: navbarPT,
Expand All @@ -101,6 +108,7 @@ i18n
about: aboutPT,
footer: footerPT,
faq: faqPT,
news: newsPT,
},
de: {
navbar: navbarDE,
Expand All @@ -113,6 +121,7 @@ i18n
about: aboutDE,
footer: footerDE,
faq: faqDE,
news: newsDE,
},
zh: {
navbar: navbarZH,
Expand All @@ -125,6 +134,7 @@ i18n
about: aboutZH,
footer: footerZH,
faq: faqZH,
news: newsZH,
},
},
supportedLngs: ["es", "en", "pt", "de", "zh"],
Expand Down
2 changes: 2 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PluginsRoute } from '@/components/community/routes/PluginsRoute'
import { CommunityRoute } from '@/components/community/routes/CommunityRoute'
import { AboutRoute } from '@/components/community/routes/AboutRoute'
import { FaqRoute } from '@/components/community/routes/FaqRoute'
import { NewsRoute } from '@/components/community/routes/NewsRoute'
import i18n from '@/app/i18n'

function getRouteFromHash(hash: string): { route: string; isSubAnchor: boolean } {
Expand Down Expand Up @@ -84,6 +85,7 @@ export default function Page() {
{route === 'plugins' && <PluginsRoute />}
{route === 'community' && <CommunityRoute />}
{route === 'faq' && <FaqRoute />}
{route === 'news' && <NewsRoute />}
{route === 'about' && <AboutRoute />}
<Footer />
</>
Expand Down
3 changes: 3 additions & 0 deletions components/community/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export function Footer() {
<li>
<a href="#faq">{t('ft.faq')}</a>
</li>
<li>
<a href="#news">{t('ft.news')}</a>
</li>
<li>
<a
href="https://github.com/DashAISoftware/dashAI/blob/production/CHANGELOG.rst"
Expand Down
3 changes: 2 additions & 1 deletion components/community/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import '@/app/i18n'
import { useStats } from '@/lib/useStats'

type Lang = 'es' | 'en' | 'pt' | 'de' | 'zh'
type Route = 'home' | 'models' | 'plugins' | 'contribute' | 'download' | 'community' | 'faq' | 'about'
type Route = 'home' | 'models' | 'plugins' | 'contribute' | 'download' | 'community' | 'faq' | 'news' | 'about'


const NAV_LINKS: { key: Route; i18nKey: string }[] = [
Expand All @@ -17,6 +17,7 @@ const NAV_LINKS: { key: Route; i18nKey: string }[] = [
{ key: 'community', i18nKey: 'nav.community' },
{ key: 'plugins', i18nKey: 'nav.plugins' },
{ key: 'faq', i18nKey: 'nav.faq' },
{ key: 'news', i18nKey: 'nav.news' },
{ key: 'about', i18nKey: 'nav.about' },
]

Expand Down
164 changes: 164 additions & 0 deletions components/community/routes/NewsRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
'use client'

import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import '@/app/i18n'
import { PRESS, FEATURED_PRESS_ID, type PressItem, type PressType } from '@/data/press'

const FILTERS: ('all' | PressType)[] = ['all', 'prensa', 'tv', 'radio', 'institucional']

const norm = (s: string) => s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')

// "2026-08-23" -> "23 AGO 2026"; "2026-08" (no day) -> "AGO 2026"
function formatDate(date: string, lang: string) {
const [y, m, d] = date.split('-').map(Number)
const opts: Intl.DateTimeFormatOptions = { month: 'short', year: 'numeric', timeZone: 'UTC' }
if (d) opts.day = 'numeric'
return new Intl.DateTimeFormat(lang, opts)
.format(new Date(Date.UTC(y, m - 1, d || 1)))
.replace('.', '')
.toUpperCase()
}

const FEATURED = PRESS.find(p => p.id === FEATURED_PRESS_ID) as PressItem

// Headlines are translated, but the linked articles aren't: flag the article's
// language when it differs from the one the site is shown in.
function LangTag({ item, lang }: { item: PressItem; lang: string }) {
const { t } = useTranslation('news')
const articleLang = item.lang ?? 'es'
if (lang.startsWith(articleLang)) return null
return <span className="tag">{t(`news.lang.${articleLang}`)}</span>
}

export function NewsRoute() {
const { t, i18n } = useTranslation('news')
const th = (key: string) => ({ __html: t(key) })
const lang = i18n.language

const [filter, setFilter] = useState<'all' | PressType>('all')
const [query, setQuery] = useState('')

const q = norm(query.trim())
const shown = useMemo(() => PRESS.filter(p =>
(filter === 'all' || p.type === filter) &&
(!q || norm([p.outlet, t(`news.item.${p.id}.headline`), t(`news.item.${p.id}.desc`)].join(' ')).includes(q)),
), [filter, q, t])

return (
<main data-route="news">

<section className="vn-hero">
<svg className="wm wm-cr wm-lg" style={{ top: '12%' }} viewBox="0 0 218.96 237.04" aria-hidden="true">
<use href="#dashai-mark" />
</svg>
<div className="wrap">
<div className="eyebrow"><span className="num">[ {t('news.bracket')} ]</span> <span>{t('news.ey')}</span></div>
<h1 dangerouslySetInnerHTML={th('news.h')} />
<p className="lead">{t('news.lead')}</p>
</div>
</section>

<section className="section" style={{ padding: '64px 0 48px' }}>
<div className="wrap">
<div className="sec-head">
<div className="left">
<div className="eyebrow"><span className="num">[ 01 ]</span> <span>{t('news.feat.ey')}</span></div>
<h2 dangerouslySetInnerHTML={th('news.feat.h')} />
<p className="lead">{t('news.feat.lead')}</p>
</div>
</div>

<div className="press-feature">
<div className="press-feature-copy">
<div className="ey">{t('news.feat.card.ey')}</div>
<h3>{t(`news.item.${FEATURED.id}.headline`)}</h3>
<p>{t('news.feat.card.p')}</p>
<div className="press-feature-meta">
<span className={`tag ${FEATURED.type}`}>{t(`news.type.${FEATURED.type}`)}</span>
<LangTag item={FEATURED} lang={lang} />
<span className="press-outlet-date">{formatDate(FEATURED.date, lang)}</span>
<a className="btn btn--sm" href={FEATURED.url} target="_blank" rel="noopener noreferrer">
{t('news.feat.card.cta')} <svg style={{ width: 13, height: 13 }}><use href="#i-arrow" /></svg>
</a>
</div>
</div>
<div className="press-quote">
<blockquote>{t('news.feat.quote')}</blockquote>
<cite><b>Susana Mondschein</b>{t('news.feat.quote.role')}</cite>
</div>
</div>
</div>
</section>

<section className="section" style={{ padding: '56px 0 72px' }}>
<div className="wrap">
<div className="sec-head">
<div className="left">
<div className="eyebrow"><span className="num">[ 02 ]</span> <span>{t('news.list.ey')}</span></div>
<h2 dangerouslySetInnerHTML={th('news.list.h')} />
<p className="lead">{t('news.list.lead')}</p>
</div>
</div>

<div className="vn-tools" style={{ marginTop: 0, marginBottom: '28px' }}>
<label className="vn-search">
<span className="sr-only">{t('news.search.sr')}</span>
<svg className="vn-search-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="M20 20l-3.5-3.5" />
</svg>
<input
type="search"
placeholder={t('news.search.ph')}
autoComplete="off"
value={query}
onChange={e => setQuery(e.target.value)}
/>
</label>
<div className="vn-chips">
{FILTERS.map(f => (
<button
key={f}
className={`vn-chip${filter === f ? ' is-on' : ''}`}
type="button"
aria-pressed={filter === f}
onClick={() => setFilter(f)}
>
{f === 'all' ? t('news.filter.all') : t(`news.type.${f}`)}
</button>
))}
</div>
<span className="vn-count">{t('news.count', { shown: shown.length, total: PRESS.length })}</span>
</div>

<div className="press-grid">
{shown.map(p => (
<a key={p.id} className="press-card" href={p.url} target="_blank" rel="noopener noreferrer">
<div className="press-outlet">
<span className={`press-mark ${p.type}`}>{p.mark}</span>
<span>
<span className="press-outlet-name">{p.outlet}</span>
<span className="press-outlet-date">{formatDate(p.date, lang)}</span>
</span>
</div>
<h3>{t(`news.item.${p.id}.headline`)}</h3>
<p>{t(`news.item.${p.id}.desc`)}</p>
<div className="press-card-foot">
<span className="press-tags">
<span className={`tag ${p.type}`}>{t(`news.type.${p.type}`)}</span>
<LangTag item={p} lang={lang} />
</span>
<span className="press-link">{t(`news.action.${p.action}`)} <svg><use href="#i-arrow" /></svg></span>
</div>
</a>
))}
</div>

<div className="vn-empty" hidden={shown.length !== 0}>{t('news.empty')}</div>
</div>
</section>

</main>
)
}
76 changes: 76 additions & 0 deletions data/press.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
export type PressType = 'prensa' | 'tv' | 'radio' | 'institucional'

export interface PressItem {
id: string
type: PressType
outlet: string
mark: string
// ISO date; 'YYYY-MM' when the outlet doesn't publish the day
date: string
url: string
// Call-to-action label: listen, watch or read
action: 'listen' | 'watch' | 'read'
// Language of the article; defaults to Spanish
lang?: string
}

// Headlines and summaries live in the `news` namespace as
// `news.item.<id>.headline` / `.desc` (the es headline is the original title).
// Rendered in this order: keep it newest first.
export const PRESS: PressItem[] = [
{
id: 'cooperativa-congreso-futuro', type: 'radio', outlet: 'Radio Cooperativa', mark: 'RC', date: '2026-08-23', action: 'listen',
url: 'https://www.cooperativa.cl/noticias/sociedad/ciencia/congreso-futuro/congreso-futuro-dashai-plataforma-de-la-u-chile-que-entrena-modelos/2026-08-23/120819.html',
},
{
id: 'cooperativa-ciencia', type: 'radio', outlet: 'Cooperativa Ciencia', mark: 'CC', date: '2026-08-18', action: 'read',
url: 'https://www.cooperativaciencia.cl/tecnologia/2026/08/18/u-de-chile-lanza-plataforma-gratuita-para-crear-ia-sin-saber-programar-y-con-total-privacidad/',
},
{
id: 'radio-polar', type: 'radio', outlet: 'Radio Polar', mark: 'RP', date: '2026-08-16', action: 'read',
url: 'https://www.radiopolar.com/universidad-chile-presenta-dashai-plataforma-gratuita-inteligencia-artificial-codigo-abierto',
},
{
id: 'la-tercera-pulso', type: 'prensa', outlet: 'La Tercera | Pulso', mark: 'LT', date: '2026-08-13', action: 'read',
url: 'https://www.latercera.com/pulso/noticia/dashai-inteligencia-artificial-desarrollada-en-chile-supera-las-20000-descargas-y-espera-apoyar-a-pymes-en-el-manejo-de-datos/',
},
{
id: 'la-voz-de-maipu', type: 'prensa', outlet: 'La Voz de Maipú', mark: 'LVM', date: '2026-08-13', action: 'read',
url: 'https://lavozdemaipu.cl/dashai-plataforma-ia-chilena-pymes/',
},
{
id: 'radio-uchile', type: 'radio', outlet: 'Diario y Radio U. de Chile', mark: 'RUCH', date: '2026-08-12', action: 'read',
url: 'https://radio.uchile.cl/2026/08/12/u-de-chile-presenta-dashai-primera-plataforma-chilena-de-inteligencia-artificial-de-codigo-abierto/',
},
{
id: 'tvn-lanzamiento', type: 'tv', outlet: 'TVN | Exponencial', mark: 'TVN', date: '2026-08-07', action: 'watch',
url: 'https://www.tvn.cl/exponencial/noticias/u-de-chile-celebra-el-lanzamiento-de-dashai-su-nueva-plataforma-de-ia',
},
{
id: 'cenia-lanzamiento', type: 'institucional', outlet: 'CENIA', mark: 'CEN', date: '2026-08-07', action: 'read',
url: 'https://cenia.cl/2026/08/07/presentan-dashai-plataforma-chilena-que-permite-trabajar-sin-entregar-datos/',
},
{
id: 'uchile', type: 'institucional', outlet: 'Universidad de Chile', mark: 'UCH', date: '2026-08-06', action: 'read',
url: 'https://uchile.cl/noticias/243234/u-de-chile-presento-dashai-plataforma-chilena-de-inteligencia-artificial',
},
{
id: 'fcfm', type: 'institucional', outlet: 'FCFM, U. de Chile', mark: 'FCFM', date: '2026-08-06', action: 'read',
url: 'https://ingenieria.uchile.cl/noticias/243214/u-de-chile-presento-dashai-plataforma-chilena-de-inteligencia-artificial',
},
{
id: 'idia', type: 'institucional', outlet: 'IDIA, U. de Chile', mark: 'IDIA', date: '2026-08', action: 'read',
url: 'https://idia.uchile.cl/2026/08/u-de-chile-presento-dashai-plataforma-chilena-que-permite-trabajar-sin-entregar-datos/',
},
{
id: 'tvn-como-funciona', type: 'tv', outlet: 'TVN | Exponencial', mark: 'TVN', date: '2026-08-03', action: 'watch',
url: 'https://www.tvn.cl/exponencial/noticias/dashai-la-app-chilena-para-usar-ia-sin-saber-programar',
},
{
id: 'cenia-gigantes', type: 'institucional', outlet: 'CENIA', mark: 'CEN', date: '2025-12-02', action: 'read',
url: 'https://cenia.cl/2025/12/02/dashai-la-herramienta-para-crear-ia-sin-saber-programar-logra-vincularse-con-gigantes-tecnologicos/',
},
]

// The launch note highlighted at the top of the page
export const FEATURED_PRESS_ID = 'uchile'
Loading
Loading