File size: 6,437 Bytes
2f00f43
af6cd33
2f00f43
 
 
 
 
 
 
 
 
 
 
 
af6cd33
2f00f43
 
af6cd33
 
 
2f00f43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
af6cd33
2f00f43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
af6cd33
2f00f43
af6cd33
 
2f00f43
af6cd33
 
2f00f43
af6cd33
 
2f00f43
af6cd33
 
2f00f43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
af6cd33
2f00f43
 
 
 
 
 
af6cd33
 
2f00f43
 
af6cd33
 
2f00f43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
af6cd33
2f00f43
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { useEffect, useState } from "react";
import { forgesight } from "@/lib/api";
import { toast } from "sonner";
import { Twitter, Linkedin, Copy, Plus, Sparkles } from "lucide-react";

export default function Journal() {
  const [items, setItems] = useState([]);
  const [title, setTitle] = useState("");
  const [body, setBody] = useState("");
  const [tags, setTags] = useState("");
  const [busy, setBusy] = useState(false);

  const load = async () => {
    try {
      const data = await forgesight.listJournal();
      setItems(data.items || []);
      if ((data.items || []).length === 0) {
        await forgesight.seedJournal();
        const r = await forgesight.listJournal();
        setItems(r.items || []);
      }
    } catch {}
  };

  useEffect(() => {
    load();
  }, []);

  const submit = async () => {
    if (!title.trim() || !body.trim()) {
      toast.error("Title + body required");
      return;
    }
    setBusy(true);
    try {
      const data = await forgesight.createJournal({
        title,
        body,
        tags: tags.split(",").map((t) => t.trim()).filter(Boolean),
      });
      setItems((prev) => [data, ...prev]);
      setTitle("");
      setBody("");
      setTags("");
      toast.success("Milestone logged + social drafts generated");
    } catch (e) {
      toast.error(e?.response?.data?.detail || "Failed to log milestone");
    } finally {
      setBusy(false);
    }
  };

  const copy = async (text, label) => {
    try {
      await navigator.clipboard.writeText(text);
      toast.success(`${label} copied`);
    } catch {
      toast.error("Copy failed");
    }
  };

  return (
    <div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="journal-page">
      <header className="mb-8">
        <div className="fs-label mb-3">§ JOURNAL · BUILD-IN-PUBLIC</div>
        <h1 className="font-display font-black tracking-tighter text-4xl md:text-5xl">Build Journal</h1>
        <p className="text-zinc-400 mt-3 max-w-2xl">
          Every milestone auto-drafts social posts — X + LinkedIn — ready to ship, hashtags and AMD / lablab mentions baked in.
        </p>
      </header>

      <div className="grid lg:grid-cols-12 gap-6">
        {/* Composer */}
        <aside className="lg:col-span-4">
          <div className="border border-white/10 bg-[#141416] p-5 fs-corners sticky top-20" data-testid="journal-composer">
            <div className="flex items-center gap-2 mb-4">
              <Sparkles className="w-3.5 h-3.5 text-[#ED1C24]" />
              <span className="fs-label">New milestone</span>
            </div>
            <div className="space-y-3">
              <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title…"
                className="w-full bg-[#0A0A0A] border border-white/10 focus:border-[#ED1C24] outline-none px-3 py-2 font-mono text-sm"
                data-testid="journal-title-input" />
              <textarea value={body} onChange={(e) => setBody(e.target.value)} rows={5} placeholder="What happened today?"
                className="w-full bg-[#0A0A0A] border border-white/10 focus:border-[#ED1C24] outline-none px-3 py-2 font-mono text-sm"
                data-testid="journal-body-input" />
              <input value={tags} onChange={(e) => setTags(e.target.value)} placeholder="tags, comma, separated"
                className="w-full bg-[#0A0A0A] border border-white/10 focus:border-[#ED1C24] outline-none px-3 py-2 font-mono text-sm"
                data-testid="journal-tags-input" />
              <button disabled={busy} onClick={submit}
                className="fs-btn fs-btn-primary w-full inline-flex items-center justify-center gap-2 disabled:opacity-50"
                data-testid="journal-submit-btn">
                {busy ? (<>Generating drafts<span className="fs-cursor" /></>) : (<><Plus className="w-4 h-4" /> Log + draft posts</>)}
              </button>
            </div>
          </div>
        </aside>

        {/* Timeline */}
        <section className="lg:col-span-8 space-y-5" data-testid="journal-timeline">
          {items.length === 0 && (
            <div className="border border-white/10 bg-[#141416] p-10 text-center font-mono text-sm text-zinc-500">
              No entries yet. Log your first milestone →
            </div>
          )}
          {items.map((e) => (
            <article key={e.id} className="border border-white/10 bg-[#141416] p-6 fs-rise" data-testid={`journal-entry-${e.id}`}>
              <div className="flex items-center justify-between mb-3 flex-wrap gap-2">
                <div className="flex items-center gap-2">
                  <span className="fs-chip fs-chip-fail">{new Date(e.created_at).toLocaleDateString()}</span>
                  {e.tags?.map((t) => (<span key={t} className="fs-chip">#{t}</span>))}
                </div>
              </div>
              <h3 className="font-display font-black tracking-tight text-xl mb-2">{e.title}</h3>
              <p className="text-sm text-zinc-300 leading-relaxed whitespace-pre-line">{e.body}</p>
              <div className="grid md:grid-cols-2 gap-3 mt-5">
                {e.x_post && (
                  <SocialCard icon={Twitter} label="X POST" text={e.x_post}
                    onCopy={() => copy(e.x_post, "X post")} testid={`x-post-${e.id}`} />
                )}
                {e.linkedin_post && (
                  <SocialCard icon={Linkedin} label="LINKEDIN POST" text={e.linkedin_post}
                    onCopy={() => copy(e.linkedin_post, "LinkedIn post")} testid={`li-post-${e.id}`} />
                )}
              </div>
            </article>
          ))}
        </section>
      </div>
    </div>
  );
}

function SocialCard({ icon: Icon, label, text, onCopy, testid }) {
  return (
    <div className="border border-white/10 bg-[#0A0A0A] p-4" data-testid={testid}>
      <div className="flex items-center justify-between mb-2">
        <div className="flex items-center gap-2">
          <Icon className="w-3.5 h-3.5 text-[#ED1C24]" />
          <span className="fs-label">{label}</span>
        </div>
        <button onClick={onCopy} className="fs-chip hover:text-white hover:border-white/40 inline-flex items-center gap-1">
          <Copy className="w-3 h-3" /> copy
        </button>
      </div>
      <div className="font-mono text-xs text-zinc-300 leading-relaxed whitespace-pre-line">{text}</div>
    </div>
  );
}