Compare commits
5 Commits
e039a496e4
...
3ba8c3340a
| Author | SHA1 | Date |
|---|---|---|
|
|
3ba8c3340a | 2 months ago |
|
|
54db0e339b | 2 months ago |
|
|
bd922c498f | 2 months ago |
|
|
39792b5dbe | 2 months ago |
|
|
665abd4dfb | 2 months ago |
13 changed files with 2068 additions and 140 deletions
@ -0,0 +1,212 @@ |
||||
import { useQuery } from "@tanstack/react-query"; |
||||
import { useParams, Link, useNavigate } from "react-router-dom"; |
||||
import { fetchTalk } from "../api"; |
||||
|
||||
/** |
||||
* TalkView component displays a single talk with full details. |
||||
*
|
||||
* It shows: |
||||
* - Speaker information with photo |
||||
* - Talk metadata (date, debate type, etc.) |
||||
* - Full text of the speech |
||||
* - Links to related resources |
||||
*/ |
||||
export function TalkView() { |
||||
const { id } = useParams<{ id: string }>(); |
||||
const navigate = useNavigate(); |
||||
|
||||
const { data: talk, isLoading, error } = useQuery({ |
||||
queryKey: ["talk", id], |
||||
queryFn: () => fetchTalk(id!), |
||||
enabled: !!id, |
||||
}); |
||||
|
||||
if (isLoading) { |
||||
return ( |
||||
<div className="talk-view"> |
||||
<div className="panel"> |
||||
<p>Laddar anförande...</p> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
if (error) { |
||||
return ( |
||||
<div className="talk-view"> |
||||
<div className="panel error-banner"> |
||||
Kunde inte ladda anförande: {error.message} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
if (!talk) { |
||||
return ( |
||||
<div className="talk-view"> |
||||
<div className="panel"> |
||||
<p>Anförande hittades inte.</p> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
// Fix image URLs from http to https
|
||||
const imageUrl = talk.person?.bild_url_192?.replace('http://', 'https://'); |
||||
|
||||
const previousTalk = talk.navigation?.previous ?? null; |
||||
const nextTalk = talk.navigation?.next ?? null; |
||||
|
||||
|
||||
// Normalise database IDs so router links stay consistent.
|
||||
const normalizeTalkId = (rawId?: string | null): string | null => { |
||||
if (!rawId) { |
||||
return null; |
||||
} |
||||
// Handle both object with _id property and plain string
|
||||
const idString = typeof rawId === 'object' && rawId !== null && '_id' in rawId
|
||||
? (rawId as any)._id
|
||||
: String(rawId); |
||||
const result = idString.startsWith("talks/") ? idString.slice(6) : idString; |
||||
return result; |
||||
}; |
||||
const previousId = normalizeTalkId(previousTalk); |
||||
const nextId = normalizeTalkId(nextTalk); |
||||
|
||||
// Use browser history when possible so the search page restores prior state.
|
||||
const handleBackClick = () => { |
||||
navigate("/"); |
||||
}; |
||||
|
||||
return ( |
||||
<div className="talk-view"> |
||||
{/* Navigation row: previous, back, next */} |
||||
<div className="talk-view__navRow"> |
||||
{/* Left: Föregående tal */} |
||||
<div className="talk-view__navCell talk-view__navCell--left talk-view__navCell--minwidth"> |
||||
{previousId && previousTalk ? ( |
||||
<Link to={`/talk/${previousId}`} className="secondary-button talk-view__navButton"> |
||||
{/* Use fontSize 1em for arrow so button height matches others */} |
||||
<span aria-hidden="true" style={{ fontSize: "1em", marginRight: "0.3em" }}>←</span> |
||||
Föregående i protokollet |
||||
</Link> |
||||
) : ( |
||||
<span className="secondary-button talk-view__navButton" style={{ visibility: "hidden" }}> </span> |
||||
)} |
||||
</div> |
||||
{/* Center: Tillbaka till sökresultat */} |
||||
<div className="talk-view__navCell talk-view__navCell--center talk-view__navCell--minwidth"> |
||||
<button |
||||
type="button" |
||||
className="secondary-button talk-view__navButton" |
||||
onClick={handleBackClick} |
||||
> |
||||
Tillbaka till sökresultat |
||||
</button> |
||||
</div> |
||||
{/* Right: Nästa tal */} |
||||
<div className="talk-view__navCell talk-view__navCell--right talk-view__navCell--minwidth"> |
||||
{nextId && nextTalk ? ( |
||||
<Link to={`/talk/${nextId}`} className="secondary-button talk-view__navButton"> |
||||
Nästa i protokollet |
||||
{/* Use fontSize 1em for arrow so button height matches others */} |
||||
<span aria-hidden="true" style={{ fontSize: "1em", marginLeft: "0.3em" }}>→</span> |
||||
</Link> |
||||
) : ( |
||||
<span className="secondary-button talk-view__navButton" style={{ visibility: "hidden" }}> </span> |
||||
)} |
||||
</div> |
||||
</div> |
||||
|
||||
{/* Speaker card */} |
||||
<div className="panel talk-view__speaker"> |
||||
{imageUrl && ( |
||||
<img
|
||||
src={imageUrl}
|
||||
alt={talk.talare} |
||||
className="talk-view__speaker-photo" |
||||
/> |
||||
)} |
||||
<div className="talk-view__speaker-info"> |
||||
<div className="talk-view__speaker-row"> |
||||
<h1>{talk.talare}</h1> |
||||
<span className="party-chip" data-party={talk.parti ?? ""}> |
||||
{talk.parti} |
||||
</span> |
||||
</div> |
||||
<div className="talk-view__speaker-meta"> |
||||
{/* Show valkrets if available */} |
||||
{talk.person?.valkrets && ( |
||||
<span className="talk-view__speaker-detail"> |
||||
{talk.person.valkrets} |
||||
</span> |
||||
)} |
||||
{/* Show status if available */} |
||||
{talk.person?.status && ( |
||||
<span className="talk-view__speaker-detail"> |
||||
{talk.person.status} |
||||
</span> |
||||
)} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
{/* Talk metadata */} |
||||
<div className="panel talk-view__metadata"> |
||||
<dl className="talk-view__meta-grid"> |
||||
<dt>Datum</dt> |
||||
<dd>{talk.datum}</dd> |
||||
|
||||
<dt>Debattyp</dt> |
||||
<dd>{talk.kammaraktivitet}</dd> |
||||
|
||||
<dt>Rubrik</dt> |
||||
<dd>{talk.avsnittsrubrik}</dd> |
||||
|
||||
{talk.titel && ( |
||||
<> |
||||
<dt>Protokoll</dt> |
||||
<dd>{talk.titel}</dd> |
||||
</> |
||||
)} |
||||
</dl> |
||||
</div> |
||||
|
||||
{/* Talk text */} |
||||
<div className="panel talk-view__text"> |
||||
<div className="talk-view__content"> |
||||
{talk.anforandetext} |
||||
</div> |
||||
</div> |
||||
|
||||
{/* Links */} |
||||
{(talk.url_session || talk.url_audio) && ( |
||||
<div className="panel talk-view__links"> |
||||
<h3>Länkar</h3> |
||||
<div className="talk-view__link-group"> |
||||
{talk.url_session && ( |
||||
<a
|
||||
href={talk.url_session}
|
||||
target="_blank"
|
||||
rel="noreferrer" |
||||
className="primary" |
||||
> |
||||
Webb-TV → |
||||
</a> |
||||
)} |
||||
{talk.url_audio && ( |
||||
<a
|
||||
href={talk.url_audio}
|
||||
target="_blank"
|
||||
rel="noreferrer" |
||||
className="primary" |
||||
> |
||||
Ljud → |
||||
</a> |
||||
)} |
||||
</div> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
@ -0,0 +1,60 @@ |
||||
from arango_client import arango |
||||
|
||||
|
||||
def clean_talk(text): |
||||
import re |
||||
|
||||
# Remove "STYLEREF Kantrubrik \* MERGEFORMAT" from the text |
||||
if "STYLEREF Kantrubrik * MERGEFORMAT" in text: |
||||
text = text.replace("STYLEREF Kantrubrik * MERGEFORMAT", "") |
||||
# Remove "- " from the text when there are text on both sides, eg. till- sammans (this comes from line breaks in Word) |
||||
text = re.sub(r"(?<=\S)-\s(?=\S)", "", text) |
||||
# Remove linebreaks in the middle of sentences |
||||
text = re.sub(r"(?<=[^\s.!?:;])\n(?=[a-zåäö])", " ", text) |
||||
return text |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
|
||||
people = arango.db.aql.execute( |
||||
"FOR p IN people RETURN {'namn': CONCAT(p.tilltalsnamn, ' ', p.efternamn), '_key': p._key}" |
||||
) |
||||
people_dict = {str(p["_key"]): p["namn"] for p in people} |
||||
|
||||
cursor = arango.db.aql.execute( |
||||
"""FOR t IN talks RETURN {'_id': t._id, 'anforandetext': t.anforandetext, 'avsnittsrubrik': t.avsnittsrubrik, 'parti': t.parti, 'intressent_id': t.intressent_id}""", |
||||
batch_size=100, |
||||
count=True, |
||||
) |
||||
|
||||
cleaned_talks = [] |
||||
n = 0 |
||||
for talk in cursor: |
||||
n += 1 |
||||
talk["anforandetext"] = clean_talk(talk.get("anforandetext", "")) |
||||
talk["avsnittsrubrik"] = clean_talk(talk.get("avsnittsrubrik", "")) |
||||
if talk.get("intressent_id") in people_dict: |
||||
talk["talare"] = people_dict[str(talk.get("intressent_id"))] |
||||
if talk["parti"] == "FP": |
||||
talk["parti"] = "L" |
||||
if talk["parti"] == "KDS": |
||||
talk["parti"] = "KD" |
||||
if talk["parti"] in [ |
||||
"TALMANNEN", |
||||
"FÖRSTE VICE TALMANNEN", |
||||
"ANDRE VICE TALMANNEN", |
||||
"TREDJE VICE TALMANNEN", |
||||
"ÅLDERSPRESIDENTEN", |
||||
"HANS MAJESTÄT KONUNGEN", |
||||
"TJÄNSTGÖRANDE ÅLDERSPRESIDENTEN", |
||||
]: |
||||
# Make first letter uppercase and rest lowercase |
||||
talk["parti"] = talk["parti"].title() |
||||
if talk["parti"] == "": |
||||
talk["parti"] = "-" |
||||
cleaned_talks.append(talk) |
||||
if len(cleaned_talks) >= 100: |
||||
arango.db.collection("talks").update_many(cleaned_talks, silent=True) |
||||
print( |
||||
f"Processed {n} talks", end="\r"), |
||||
cleaned_talks = [] |
||||
Loading…
Reference in new issue