Skip to content

Commit 13aa9c4

Browse files
add search page, related frases, linkedin share, draft preview, map improvements, new posts
1 parent 9214a3b commit 13aa9c4

File tree

17 files changed

+722
-75
lines changed

17 files changed

+722
-75
lines changed

.github/workflows/broadcast.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Broadcast nuevo post
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "src/content/posts/**.mdx"
8+
9+
jobs:
10+
broadcast:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 2
16+
17+
- name: Detectar post nuevo
18+
id: detect
19+
run: |
20+
NEW_POST=$(git diff --name-only HEAD~1 HEAD \
21+
-- 'src/content/posts/*.mdx' | head -1)
22+
echo "post=$NEW_POST" >> $GITHUB_OUTPUT
23+
if [ -n "$NEW_POST" ]; then
24+
echo "found=true" >> $GITHUB_OUTPUT
25+
else
26+
echo "found=false" >> $GITHUB_OUTPUT
27+
fi
28+
29+
- name: Extraer frontmatter del post
30+
if: steps.detect.outputs.found == 'true'
31+
id: frontmatter
32+
run: |
33+
POST_FILE="${{ steps.detect.outputs.post }}"
34+
TITLE=$(grep '^title:' "$POST_FILE" | \
35+
sed 's/title: *//' | tr -d '"')
36+
DESC=$(grep '^description:' "$POST_FILE" | \
37+
sed 's/description: *//' | tr -d '"')
38+
SLUG=$(basename "$POST_FILE" .mdx)
39+
QUOTE=$(grep '^quote:' "$POST_FILE" | \
40+
sed 's/quote: *//' | tr -d '"')
41+
echo "title=$TITLE" >> $GITHUB_OUTPUT
42+
echo "description=$DESC" >> $GITHUB_OUTPUT
43+
echo "slug=$SLUG" >> $GITHUB_OUTPUT
44+
echo "quote=$QUOTE" >> $GITHUB_OUTPUT
45+
46+
- name: Enviar broadcast
47+
if: steps.detect.outputs.found == 'true'
48+
run: |
49+
curl -X POST \
50+
"${{ secrets.VERCEL_URL }}/api/broadcast" \
51+
-H "Content-Type: application/json" \
52+
-d '{
53+
"secret": "${{ secrets.BROADCAST_SECRET }}",
54+
"postSlug": "${{ steps.frontmatter.outputs.slug }}",
55+
"postTitle": "${{ steps.frontmatter.outputs.title }}",
56+
"postDescription": "${{ steps.frontmatter.outputs.description }}",
57+
"postQuote": "${{ steps.frontmatter.outputs.quote }}",
58+
"postUrl": "https://vitologic.vercel.app/blog/${{ steps.frontmatter.outputs.slug }}"
59+
}'

src/components/BuscarPage.tsx

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import Fuse from 'fuse.js';
3+
import { format } from 'date-fns';
4+
import { es } from 'date-fns/locale';
5+
import CopyButton from './CopyButton';
6+
import StoryGenerator from './StoryGenerator';
7+
8+
interface SearchItem {
9+
id: string;
10+
title: string;
11+
description: string;
12+
tags: string[];
13+
date: string;
14+
type: 'post' | 'frase';
15+
url: string;
16+
slug?: string;
17+
}
18+
19+
const HighlightText = ({ text, query }: { text: string; query: string }) => {
20+
if (!query) return <>{text}</>;
21+
const parts = text.split(new RegExp(`(${query})`, 'gi'));
22+
return (
23+
<>
24+
{parts.map((part, i) =>
25+
part.toLowerCase() === query.toLowerCase() ? (
26+
<mark key={i} className="bg-lime-400/20 text-lime-400 px-1 rounded inline-block bg-transparent">{part}</mark>
27+
) : (
28+
part
29+
)
30+
)}
31+
</>
32+
);
33+
};
34+
35+
export default function BuscarPage({ items }: { items: SearchItem[] }) {
36+
const [query, setQuery] = useState('');
37+
const [activeTab, setActiveTab] = useState<'posts' | 'frases'>('posts');
38+
const [results, setResults] = useState<SearchItem[]>([]);
39+
const fuseRef = useRef<Fuse<SearchItem> | null>(null);
40+
41+
useEffect(() => {
42+
// Read query from URL on mount
43+
const params = new URLSearchParams(window.location.search);
44+
const q = params.get('q');
45+
if (q) setQuery(q);
46+
}, []);
47+
48+
useEffect(() => {
49+
// Update URL when query changes
50+
const url = new URL(window.location.href);
51+
if (query) {
52+
url.searchParams.set('q', query);
53+
} else {
54+
url.searchParams.delete('q');
55+
}
56+
window.history.replaceState({}, '', url.toString());
57+
}, [query]);
58+
59+
useEffect(() => {
60+
fuseRef.current = new Fuse(items, {
61+
keys: ['title', 'description', 'tags'],
62+
threshold: 0.3,
63+
includeScore: true,
64+
ignoreLocation: true,
65+
});
66+
}, [items]);
67+
68+
useEffect(() => {
69+
if (query.trim() === '') {
70+
setResults(items);
71+
return;
72+
}
73+
if (fuseRef.current) {
74+
const searchResults = fuseRef.current.search(query).map(r => r.item);
75+
setResults(searchResults);
76+
}
77+
}, [query, items]);
78+
79+
const posts = results.filter(r => r.type === 'post');
80+
const frases = results.filter(r => r.type === 'frase');
81+
const activeResults = activeTab === 'posts' ? posts : frases;
82+
83+
const handleSuggestion = (suggestion: string) => {
84+
setQuery(suggestion);
85+
};
86+
87+
return (
88+
<div className="w-full">
89+
<div className="mb-12 relative">
90+
<svg className="absolute left-6 top-1/2 -translate-y-1/2 text-[#525252] w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
91+
<input
92+
type="text"
93+
autoFocus
94+
value={query}
95+
onChange={e => setQuery(e.target.value)}
96+
placeholder="Buscar ideas, reflexiones, frases..."
97+
className="w-full bg-[#111111] border border-[#1f1f1f] focus:border-lime-400 focus:ring-1 focus:ring-lime-400 rounded-xl text-[#f5f5f5] text-xl md:text-2xl px-16 py-6 outline-none transition-all placeholder:text-[#525252]"
98+
/>
99+
{query && (
100+
<button
101+
onClick={() => setQuery('')}
102+
className="absolute right-6 top-1/2 -translate-y-1/2 text-[#525252] hover:text-[#f5f5f5] p-2"
103+
>
104+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
105+
</button>
106+
)}
107+
</div>
108+
109+
<div className="flex border-b border-[#1f1f1f] mb-8">
110+
<button
111+
onClick={() => setActiveTab('posts')}
112+
className={`px-6 py-4 font-medium text-sm transition-colors border-b-2 ${activeTab === 'posts' ? 'border-lime-400 text-[#f5f5f5]' : 'border-transparent text-[#525252] hover:text-[#a3a3a3]'}`}
113+
>
114+
Posts ({posts.length})
115+
</button>
116+
<button
117+
onClick={() => setActiveTab('frases')}
118+
className={`px-6 py-4 font-medium text-sm transition-colors border-b-2 ${activeTab === 'frases' ? 'border-lime-400 text-[#f5f5f5]' : 'border-transparent text-[#525252] hover:text-[#a3a3a3]'}`}
119+
>
120+
Frases ({frases.length})
121+
</button>
122+
</div>
123+
124+
<div className="mb-8 text-[#737373] text-sm">
125+
{query ? (
126+
<span>{activeResults.length} resultados para "{query}"</span>
127+
) : (
128+
<span>Mostrando todo el contenido ({activeResults.length})</span>
129+
)}
130+
</div>
131+
132+
{activeResults.length === 0 ? (
133+
<div className="text-center py-20 border border-dashed border-[#1f1f1f] rounded-xl bg-[#0a0a0a]">
134+
<p className="text-[#a3a3a3] text-lg mb-4">Ningún resultado para "{query}"</p>
135+
<p className="text-[#525252] text-sm mb-6">Sugerencias para explorar:</p>
136+
<div className="flex flex-wrap justify-center gap-3">
137+
{['poder', 'biblia', 'silencio', 'envidia', 'reflexiones'].map(suggestion => (
138+
<button
139+
key={suggestion}
140+
onClick={() => handleSuggestion(suggestion)}
141+
className="px-4 py-2 rounded-full border border-[#1f1f1f] bg-[#111111] text-[#737373] hover:text-[#a3e635] hover:border-lime-400/30 text-sm transition-colors"
142+
>
143+
{suggestion}
144+
</button>
145+
))}
146+
</div>
147+
</div>
148+
) : (
149+
<div className={activeTab === 'frases' ? 'columns-1 md:columns-2 gap-6' : 'flex flex-col gap-6'}>
150+
{activeResults.map(item => (
151+
activeTab === 'posts' ? (
152+
<a
153+
key={item.id}
154+
href={item.url}
155+
className="block p-6 rounded-xl border border-[#1f1f1f] bg-[#111111] hover:border-[#333] transition-colors group"
156+
>
157+
<div className="flex items-center gap-3 text-xs text-[#525252] uppercase tracking-widest mb-3">
158+
<time dateTime={item.date}>
159+
{format(new Date(item.date), "d MMM yyyy", { locale: es })}
160+
</time>
161+
{item.tags && item.tags.length > 0 && (
162+
<>
163+
<span>·</span>
164+
<span className="text-lime-400/70">{item.tags[0]}</span>
165+
</>
166+
)}
167+
</div>
168+
<h2 className="text-2xl font-bold text-[#f5f5f5] group-hover:text-lime-400 transition-colors mb-3">
169+
<HighlightText text={item.title} query={query} />
170+
</h2>
171+
<p className="text-[#a3a3a3] leading-relaxed line-clamp-2">
172+
<HighlightText text={item.description} query={query} />
173+
</p>
174+
</a>
175+
) : (
176+
<div
177+
key={item.id}
178+
className="break-inside-avoid mb-6 p-6 rounded-xl border border-[#1f1f1f] bg-[#111111] hover:border-[#333] transition-colors flex flex-col justify-between"
179+
>
180+
<p className="text-lg text-[#f5f5f5] font-medium leading-relaxed italic mb-6">
181+
"<HighlightText text={item.description} query={query} />"
182+
</p>
183+
<div className="flex items-center justify-between mt-auto">
184+
<a href={item.url} className="text-xs px-2.5 py-1 rounded bg-[#1f1f1f] text-[#a3a3a3] hover:bg-lime-400/10 hover:text-lime-400 transition-colors">
185+
#{item.tags[0]}
186+
</a>
187+
<div className="flex items-center gap-2">
188+
<StoryGenerator texto={item.description} />
189+
<CopyButton text={item.description} />
190+
</div>
191+
</div>
192+
</div>
193+
)
194+
))}
195+
</div>
196+
)}
197+
</div>
198+
);
199+
}

src/components/Header.astro

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,21 @@ const isActive = (href: string) =>
6060
style="padding-left: max(2rem, calc((100vw - 1200px) / 2)); padding-right: max(2rem, calc((100vw - 1200px) / 2)); border-bottom-width: 1px;"
6161
>
6262
<!-- Logo Izquierda -->
63-
<a href="/" class="flex flex-col flex-shrink-0 group">
63+
<a href="/" class="flex flex-col flex-shrink-0 group relative">
6464
<div class="flex items-center tracking-tight leading-none mb-1">
6565
<span class="text-[#f5f5f5] font-[800] text-xl">Vito</span>
6666
<span class="text-lime-400 font-[800] text-xl">Logic</span>
67+
{
68+
import.meta.env.DEV && (
69+
<div
70+
class="ml-2 relative flex items-center justify-center group"
71+
title="Modo desarrollo"
72+
>
73+
<span class="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-red-400 opacity-75" />
74+
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500" />
75+
</div>
76+
)
77+
}
6778
</div>
6879
<span
6980
class="text-[#525252] text-[10px] uppercase tracking-[0.25em] font-medium leading-none"

0 commit comments

Comments
 (0)