Compare commits

..

3 commits

Author SHA1 Message Date
1eb15058d7 Spent a hour fixing the login & registing system & also finding errors
Some checks are pending
Build and Push Docker Image / build-and-push (push) Waiting to run
in the custom groq api system (also a typo wasted way too much time)
2025-06-07 15:49:28 +08:00
a8d675e2ae AboutNewsOrg & newsView is now abosolutely working !!! :D Also updated
some README as well :)
2025-06-07 14:51:40 +08:00
3d9e69d4fc Add translating system to aboutNewsOrg. (And it didn't work ;( 2025-06-07 14:23:38 +08:00
8 changed files with 135 additions and 30 deletions

View file

@ -18,7 +18,7 @@ Reverse engineering 文檔: [about](/about/)
## 在部署之前,請先知道:
此程式碼絕對不是為在 Vercel 或 Netlify 上啟動而設計的它現在在主網站程式碼中具有crawling而且整個「快取功能」都基於Ram所以請不要使用這些平台對於 Zeabur 來說您的成本一定會比較貴一點。網址https://news.yuanhau.com 託管在我自己的infra上你也應該這麼做。可以在Yahoo拍賣、蝦皮取得伺服器來執行這個程式。
## Note for deing
## Note for developing
The desktop enviroment is super unstable when even using a beefy computer, even so, the desktop will lag when opening the newsView, like it's just hates being in a dev env. Prod app works tho, so you can demo it using `bun run build && bun run preview` for demoing. Please don't file a issue request for this matter. If you have the fix, please contribute using Github PRs.
## 為什麼?

View file

@ -15,6 +15,9 @@ Deploy: [via docker compose](/deploy.md)
## Demo:
You can try out the app RIGHT NOW via this link: https://yhw.tw/news?goto=desktop
## Using Translations:
A few pages now contains translations, like the aboutNewsOrg & newsView pages. This project currently is using Google Translate. However, muiti translate platform support is coming soon™ (If you login with your account). The translations are not accrate at all, like something that should be `I just want to write about sports` becomes `I just want to write`, that bro, what is even that?
## Before deploying, please know this:
This code is absolutly NOT designed to be spinned up at Vercel or Netlify, it has the scraping system now inside of the main website code, oh also the entire "caching feature" is based in memory, so please don't use those platforms, for Zeabur your cost might be expensive. idk, I haven't tried hit yet. The web url: https://news.yuanhau.com is hosted on my own infra, you should too. Please get a server off of yahoo 拍賣, 蝦皮 or eBay to do so.

View file

@ -6,6 +6,12 @@ import { ScrambleTextPlugin } from "gsap/dist/ScrambleTextPlugin";
gsap.registerPlugin(ScrambleTextPlugin);
const loading = ref(true);
const { t, locale } = useI18n();
import translate from "translate";
interface translateInterfaceText {
translateText: string;
}
const translateItem: Record<string, translateInterfaceText> = {};
const emit = defineEmits([
"windowopener",
@ -19,6 +25,7 @@ const props = defineProps({
type: String,
required: true,
},
applyForTranslation: Boolean,
});
const staticProps = props;
@ -59,8 +66,62 @@ watch(
const openNews = (url: string, titleName: string) => {
emit("openArticles", url, titleName);
};
const startTranslating = async (text: string) => {
try {
console.log(text);
translateItem[text] = {
translateText: await translate(text, { from: "zh", to: "en" }),
};
console.log(translateItem[text]);
} catch (error) {
console.error("Translation failed:", error);
traslateFailed.value = true;
translateItem[text] = { translateText: text }; // fallback to original text
}
};
// Translating logic
const translateText = ref(false);
const translatedBefore = ref(false);
const traslateFailed = ref(false);
const displayTranslatedText = ref(false);
const loadingTranslations = ref(false);
watch(
() => props.applyForTranslation,
(value) => {
if (value === true) {
translateText.value = true;
if (!fetchNewsOrgInfo.value) {
return;
}
if (translatedBefore.value === true) {
displayTranslatedText.value = true;
return;
}
loadingTranslations.value = true;
startTranslating(fetchNewsOrgInfo.value?.title);
startTranslating(fetchNewsOrgInfo.value?.description);
for (const articles of fetchNewsOrgInfo.value?.articles) {
startTranslating(articles.title.replaceAll("獨家專欄》", ""));
startTranslating(articles.date);
}
// NOT retranslating AGAIN when disabling the feat
translatedBefore.value = true;
setTimeout(() => {
displayTranslatedText.value = true;
loadingTranslations.value = false;
}, 3000);
} else {
translateText.value = false;
displayTranslatedText.value = false;
}
},
); // Translate when requested?
</script>
<template>
<div v-if="loadingTranslations">Loading...</div>
<div>
<div class="text-center align-center justify-center">
<div
@ -76,7 +137,11 @@ const openNews = (url: string, titleName: string) => {
class="text-4xl font-bold m-2 text-center"
ref="orgNameAnimation"
>
{{ fetchNewsOrgInfo?.title }}
{{
displayTranslatedText
? translateItem[fetchNewsOrgInfo?.title].translateText
: fetchNewsOrgInfo?.title
}}
</h1>
<div v-if="pending" class="flex flex-col gap-2 m-1 mt-5">
@ -85,7 +150,11 @@ const openNews = (url: string, titleName: string) => {
<div class="h-4 bg-gray-200 animate-pulse rounded w-4/6"></div>
</div>
<span v-else class="text-ms m-1 mt-5 text-left text-wrap">
{{ fetchNewsOrgInfo?.description }}
{{
displayTranslatedText
? translateItem[fetchNewsOrgInfo?.description].translateText
: fetchNewsOrgInfo?.description
}}
</span>
</div>
</div>
@ -109,9 +178,16 @@ const openNews = (url: string, titleName: string) => {
<div>
<div class="flex flex-col">
<span class="title text-bold texxt-sm">{{
item.title.replaceAll("獨家專欄》", "")
displayTranslatedText
? translateItem[item.title.replaceAll("獨家專欄》", "")]
.translateText
: item.title.replaceAll("獨家專欄》", "")
}}</span>
<span class="date text-xs">{{
displayTranslatedText
? translateItem[item.date].translateText
: item.date
}}</span>
<span class="date text-xs">{{ item.date }}</span>
</div>
</div>
</button>

View file

@ -216,6 +216,12 @@ const openPublisher = (slug: string, title: string) => {
};
const isLoading = computed(() => contentArray.value.length === 0);
const testmessage = await translate("嗨", { from: "zh", to: "en" });
const shouldHideItem = (item) => {
return (
item.contentType !== "GENERAL" ||
item.publisher?.toLowerCase().includes("line")
);
};
</script>
<template>
<div class="justify-center align-center text-center">
@ -257,7 +263,7 @@ const testmessage = await translate("嗨", { from: "zh", to: "en" });
<div>
<!-- Loading State -->
<template v-if="isLoading">
<div v-for="n in 5" :key="n" class="p-2 bg-gray-200 rounded m-1">
<div v-for="n in 7" :key="n" class="p-2 bg-gray-200 rounded m-1">
<!-- Title Skeleton -->
<div
class="h-8 bg-gray-300 animate-pulse rounded-lg w-3/4 mx-auto mb-2"
@ -303,9 +309,9 @@ const testmessage = await translate("嗨", { from: "zh", to: "en" });
<div
v-for="item in contentArray"
:key="item.id"
:class="item.contentType !== 'GENERAL' && 'hidden'"
:class="shouldHideItem(item) && 'hidden'"
>
<div class="p-2 bg-gray-200 rounded m-1 p-1">
<div class="p-2 bg-gray-200 rounded m-1">
<h1
class="text-2xl text-bold"
:class="getCheckResult(item.title) ? 'text-red-600' : ''"

View file

@ -30,6 +30,8 @@ const likeart = ref([]);
const translateText = ref(false);
const translatedBefore = ref(false);
const traslateFailed = ref(false);
const displayTranslatedText = ref(false);
const loadingTranslations = ref(false);
watch(
() => props.applyForTranslation,
(value) => {
@ -39,8 +41,10 @@ watch(
return;
}
if (translatedBefore.value === true) {
displayTranslatedText.value = true;
return;
}
loadingTranslations.value = true;
startTranslating(data.value.title);
startTranslating(data.value.origin);
startTranslating(data.value.author);
@ -49,8 +53,13 @@ watch(
}
// NOT retranslating AGAIN when disabling the feat
translatedBefore.value = true;
setTimeout(() => {
displayTranslatedText.value = true;
loadingTranslations.value = false;
}, 3000);
} else {
translateText.value = false;
displayTranslatedText.value = false;
}
},
); // Translate when requested?
@ -103,6 +112,8 @@ const aiSummary = async () => {
<button></button>
</div>
</div>
<!--TODO: Get a better animation later.-->
<div v-if="loadingTranslations">Loading...</div>
<div
class="justify-center align-center text-center flex flex-col md:flex-row flex-wrap"
>
@ -110,18 +121,20 @@ const aiSummary = async () => {
<div class="group">
<h2 class="text-3xl text-bold">
{{
translateText ? translateItem[data.title].translateText : data.title
displayTranslatedText
? translateItem[data.title].translateText
: data.title
}}
</h2>
<span
class="text-lg text-bold flex flex-row justify-center text-center align-center"
><NewspaperIcon class="w-7 h-7 p-1" />{{
translateText
displayTranslatedText
? translateItem[data.origin].translateText
: data.origin
}}
<UserIcon class="w-7 h-7 p-1" />{{
translateText
displayTranslatedText
? translateItem[data.author].translateText
: data.author
}}</span
@ -131,7 +144,7 @@ const aiSummary = async () => {
<img v-if="data.images[0]" :src="data.images[0]" class="rounded" />
</div>
<div class="text-center" v-for="item in data.paragraph">
{{ translateText ? translateItem[item]?.translateText : item }}
{{ displayTranslatedText ? translateItem[item]?.translateText : item }}
</div>
</div>
<div class="flex flex-col w-full justify-center align-center text-center">

View file

@ -11,6 +11,7 @@ export default defineEventHandler(async (event) => {
const protocol = getRequestProtocol(event);
const slug = getRouterParam(event, "slug");
const userToken = getCookie(event, "token") || "";
console.log("Token: ", userToken);
const doesTheUserHasACustomGroqApiAndWhatIsIt =
await checkIfUserHasCustomGroqKey(userToken);
let groqClient = groq;

View file

@ -46,13 +46,18 @@ export default defineEventHandler(async (event) => {
};
}
const createOtherFields = await sql`
insert into user_other_data(user_id, user, translate_enabled, translate_provider, remove_translate_popup)
values (${userUUID}, ${username}, false, 'google', false)
insert into user_other_data(user_id, username, translate_enabled, translate_provider, remove_translate_popup, starred_news)
values (${userUUID}, ${username}, false, 'google', false, '{}'::JSON)
`;
const newToken = uuidv4();
await sql`
INSERT INTO usertokens (username, token)
VALUES (${username}, ${newToken})
`;
setCookie(event, "token", newToken);
return {
user: fetchUserInfo,
token: newToken,
user: fetchUserInfoAgain,
};
} else {
const isValid = await argon2.verify(
@ -64,19 +69,20 @@ export default defineEventHandler(async (event) => {
error: "PASSWORD_NO_MATCH",
};
}
}
const newToken = uuidv4();
const fetchUserInfoAgain = await sql`
select * from users
where username = ${username}`;
await sql`
INSERT INTO usertokens (user, token)
VALUES ('${fetchUserInfo[0].username}', '${newToken}')
const newToken = uuidv4();
const fetchUserInfoAgain = await sql`
select * from users
where username = ${username}`;
await sql`
INSERT INTO usertokens (username, token)
VALUES (${fetchUserInfoAgain[0].username}, ${newToken})
`;
setCookie(event, "token", newToken);
return {
user: fetchUserInfoAgain,
};
setCookie(event, "token", newToken);
return {
user: fetchUserInfoAgain,
};
}
} catch (e) {
console.log(e);
return {

View file

@ -9,7 +9,7 @@ export async function checkIfUserHasCustomGroqKey(token?: string) {
}
const checkRealToken = await sql`
select * from usertokens
where tokens=${token}
where token = ${token}
`;
if (checkRealToken.length === 0) {
return {
@ -28,6 +28,6 @@ export async function checkIfUserHasCustomGroqKey(token?: string) {
}
return {
status: true,
customAPi: fetchUserToken[0].groq_api_key,
customApi: fetchUserToken[0].groq_api_key,
};
}