mirror of
https://github.com/hpware/news-analyze.git
synced 2025-06-23 15:51:01 +08:00
Compare commits
19 Commits
7ad8caeed8
...
b356afe766
Author | SHA1 | Date | |
---|---|---|---|
b356afe766 | |||
028b545374 | |||
3abfe46464 | |||
d2099074a7 | |||
61a7ecbf12 | |||
7314a5fa8a | |||
a4a3822a49 | |||
bae0d3b8dc | |||
9bf177f971 | |||
64f4babe95 | |||
083fae51de | |||
4d49554a0e | |||
b8438f7f33 | |||
48f897ed63 | |||
9b9ebb7f50 | |||
feb679c3ef | |||
8032c3faae | |||
231a7ce251 | |||
0298a5ae90 |
29
.dev.env
Normal file
29
.dev.env
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# For prod use please use the .env.example file.
|
||||||
|
# Please use .dev.env as an starting point. Rename it to .env and fill in the values, the application needs it.
|
||||||
|
|
||||||
|
# This is the developmemnt use .env file.
|
||||||
|
|
||||||
|
# S3 INFO
|
||||||
|
S3_ACCESS_KEY=""
|
||||||
|
S3_SECRET_KEY=""
|
||||||
|
S3_BUCKETNAME=""
|
||||||
|
S3_ENDPOINT=""
|
||||||
|
|
||||||
|
# GITHUB OAUTH (NOT WORKING 4n)
|
||||||
|
NUXT_GITHUB_CLIENT_ID=""
|
||||||
|
NUXT_GITHUB_CLIENT_SECRET=""
|
||||||
|
|
||||||
|
# GLOBAL DATABASE
|
||||||
|
POSTGRES_URL=""
|
||||||
|
|
||||||
|
# GROQ API KEY
|
||||||
|
GROQ_API_KEY=""
|
||||||
|
|
||||||
|
# PASSWORD SALT
|
||||||
|
PASSWORD_HASH_SALT=""
|
||||||
|
|
||||||
|
# CF TURNSTILE
|
||||||
|
NUXT_CF_TURNSTILE_SITE_KEY=""
|
||||||
|
NUXT_CF_TURNSTILE_SECRET_KEY=""
|
||||||
|
|
||||||
|
NUXT_DEV_ENV=true
|
@ -1,3 +1,4 @@
|
|||||||
|
# For development use, please use the .dev.env file.
|
||||||
# Please use .env.exmaple as an starting point. Rename it to .env and fill in the values, the application needs it.
|
# Please use .env.exmaple as an starting point. Rename it to .env and fill in the values, the application needs it.
|
||||||
|
|
||||||
# This is the default .env file.
|
# This is the default .env file.
|
||||||
@ -24,3 +25,5 @@ PASSWORD_HASH_SALT=""
|
|||||||
# CF TURNSTILE
|
# CF TURNSTILE
|
||||||
NUXT_CF_TURNSTILE_SITE_KEY=""
|
NUXT_CF_TURNSTILE_SITE_KEY=""
|
||||||
NUXT_CF_TURNSTILE_SECRET_KEY=""
|
NUXT_CF_TURNSTILE_SECRET_KEY=""
|
||||||
|
|
||||||
|
NUXT_DEV_ENV=false
|
||||||
|
4
GOALS_BEFORE_NEXT_DEVLOG.md
Normal file
4
GOALS_BEFORE_NEXT_DEVLOG.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Goals before the next devlog
|
||||||
|
(Hopefuly it can be done like on jun/7?)
|
||||||
|
1. Get the custom Groq api thingy work
|
||||||
|
2. Get Translation into news, newsView, aboutNewsorg & sources
|
235
README.ZH_TW.md
Normal file
235
README.ZH_TW.md
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# 新聞解析 / News Analyze
|
||||||
|
|
||||||
|
[English Version](/README.md) [繁體中文版](/README.ZH_TW.md)
|
||||||
|
|
||||||
|
   
|
||||||
|
|
||||||
|
一個 Neighborhood 專案。 現在只提供電腦的版本,手機版的目前不支援。
|
||||||
|
|
||||||
|
UI設計圖: [PDF 檔案](/design.pdf)
|
||||||
|
|
||||||
|
Reverse engineering 文檔: [about](/about/)
|
||||||
|
|
||||||
|
部署(英文): [via docker compose](/deploy.md)
|
||||||
|
|
||||||
|
## Demo:
|
||||||
|
你可以使用以下的連結來**立即**使用: https://yhw.tw/news?goto=desktop
|
||||||
|
|
||||||
|
## 在部署之前,請先知道:
|
||||||
|
此程式碼絕對不是為在 Vercel 或 Netlify 上啟動而設計的,它現在在主網站程式碼中具有crawling,而且整個「快取功能」都基於Ram,所以請不要使用這些平台,對於 Zeabur 來說,您的成本一定會比較貴一點。網址:https://news.yuanhau.com 託管在我自己的infra上,你也應該這麼做。可以在Yahoo拍賣、蝦皮取得伺服器來執行這個程式。
|
||||||
|
|
||||||
|
## Note for deing
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 為什麼?
|
||||||
|
|
||||||
|
我們使用這個新聞來舉例:
|
||||||
|
|
||||||
|
```
|
||||||
|
朱立倫批政府像希特勒德國在台協會:不應為政治扭曲歷史| 政治 - 中央社 CNA
|
||||||
|
5/7/2025, 11:17:00 PM
|
||||||
|
類似新聞:
|
||||||
|
- 朱立倫批政府像希特勒德國在台協會:不應為政治扭曲歷史| 政治 - 中央社 CNA
|
||||||
|
- 快訊/硬起來!朱立倫回擊德國在台協會:外國政府不該干預各國內政 - 富房網
|
||||||
|
- 綠委憂希特勒說釀災 外交部:全力向駐台館處說明 - 經濟日報
|
||||||
|
- 「朱立倫道歉」!亂比喻遭德國、以色列譴責 民進黨:賠上台灣國際名譽 - 奇摩新聞
|
||||||
|
- 洪聖斐觀點》獨裁餘毒罵人「法西斯」 朱立倫東施效顰共產黨| 政治 - Newtalk新聞
|
||||||
|
```
|
||||||
|
|
||||||
|
你會看到許多觀點,但不知道這些新聞為什麼會寫比較偏見的文章。
|
||||||
|
|
||||||
|
## 靈感來自
|
||||||
|
|
||||||
|
- puter.com
|
||||||
|
- Perplexity
|
||||||
|
- Ground.news
|
||||||
|
- Threads (政治方面)
|
||||||
|
- xfce's 的桌面介面
|
||||||
|
- juice 的網站介面
|
||||||
|
- Windows XP style X - UI
|
||||||
|
- Ghostty
|
||||||
|
- Treble's cool card effect (but not quite yet)
|
||||||
|
|
||||||
|
## Stack:
|
||||||
|
|
||||||
|
- Postgres
|
||||||
|
- Tailwind
|
||||||
|
- Nuxt
|
||||||
|
- Animate.css
|
||||||
|
- GSAP
|
||||||
|
- Minio S3
|
||||||
|
- Nuxt i18n
|
||||||
|
- BunJS
|
||||||
|
- Groq
|
||||||
|
- Custom Infra
|
||||||
|
- Docker
|
||||||
|
- Docker Compose
|
||||||
|
- GitHub Actions
|
||||||
|
- Line Today (非正式 APIs)
|
||||||
|
- Cheerio
|
||||||
|
- Sentry
|
||||||
|
- Umami Analytics
|
||||||
|
- Prettier
|
||||||
|
|
||||||
|
## 預覽系統:
|
||||||
|
### 首頁:
|
||||||
|

|
||||||
|
|
||||||
|
### 桌面程式:
|
||||||
|

|
||||||
|
|
||||||
|
## 如何在我的電腦上運行?
|
||||||
|
|
||||||
|
1. 第一, 把 `.env.example` 改名到 `.env` 並填空白處
|
||||||
|
2. 使用 `bun install` 來安裝需要的套件。
|
||||||
|
3. 跑 `bun run createDatabase` 來創建資料庫
|
||||||
|
4. 跑 `bun run build` 來 build 程式
|
||||||
|
5. 跑 `bun run preview` 來開 preview 的伺服器程式
|
||||||
|
6. 在瀏覽器打開 `http://localhost:3000` 就可用了
|
||||||
|
|
||||||
|
## 有問題?
|
||||||
|
<div>
|
||||||
|
可以使用 GitHub Issues<br/>
|
||||||
|
------ 或 ------<br/>
|
||||||
|
使用這個表單:<a href="https://yhw.tw/SaBta">https://yhw.tw/SaBta</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 為什麼使用 Line Today?
|
||||||
|
<!--[PDF](https://hc-cdn.hel1.your-objectstorage.com/s/v3/c6cef365b20a3faff96540db9b6a9871b60e8e06_cn_b2b_line_today_preroll_______sales_kit_2024.pdf)-->
|
||||||
|
[LINE 官方連結](https://vos.line-scdn.net/lbstw-static/images/uploads/download_files/74db75f34e30dee20af94c7d970f2a02/CN_B2B_LINE%20TODAY%20Preroll%E5%BB%A3%E5%91%8A%20Sales%20kit_2024.pdf)
|
||||||
|
|
||||||
|
在 LINE 自己的口中 「LINE TODAY是消費者獲取各式知識資訊的重要入⼝」,當然可以讓新聞媒體給他新聞賺錢,所以很多Article多會在 LINE Today 上
|
||||||
|
|
||||||
|
## 免費的 API!
|
||||||
|
API 資訊: https://news.yuanhau.com/apis
|
||||||
|
|
||||||
|
如果您只是想將其交給AI並叫他幫你做網站,這裡是可以免費使用的 API,歡迎你做比我的更好的東西。請給我credit就好ㄌ:)
|
||||||
|
|
||||||
|
LLM Line
|
||||||
|
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
https://news.yuanhau.com/api/tabs for fetching Tabs
|
||||||
|
|
||||||
|
The API looks like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"text": "焦點",
|
||||||
|
"url": "top",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
...
|
||||||
|
{
|
||||||
|
"text": "追蹤",
|
||||||
|
"url": "subscription",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cached": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
https://news.yuanhau.com/api/home/lt?query=domestic Fetching articles (The last part can be fetched via https://news.yuanhau.com/datainfo/linetodayjsondata.json and DON'T remove the ?query=)
|
||||||
|
|
||||||
|
The API looks like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuids": [
|
||||||
|
"4377aa43-9614-485f-ae6c-9c5f4f625ceb",
|
||||||
|
],
|
||||||
|
"nuuid": [
|
||||||
|
"news_cat:5epcfp46048f3c5cp03zo4p6"
|
||||||
|
],
|
||||||
|
"uuidData": [
|
||||||
|
{
|
||||||
|
"id": "XXXXXXXXX",
|
||||||
|
"title": "XXXXXXXX",
|
||||||
|
"publisher": "XXXXX",
|
||||||
|
"publisherId": "XXXXXX",
|
||||||
|
"publishTimeUnix": 1748321220000,
|
||||||
|
"contentType": "GENERAL",
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "IMAGE",
|
||||||
|
"hash": "0hpzwfjHPRL1VKHzEH3C5QAhZJLDp5czxWLil-YTQeNBoRWGtWAHEiYwZ8LzdkJyxRPhIrUgleNxo_RGliEBk8ZgoeODUSeipQACAkTzMWOjcSXy54KiNoTx8"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"hash": "XXXXXX"
|
||||||
|
},
|
||||||
|
"categoryId": 100262,
|
||||||
|
"categoryName": "XX",
|
||||||
|
"shortDescription": "..."
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"nuuiddata": [
|
||||||
|
{
|
||||||
|
"id": "news_cat:5epcfp46048f3c5cp03zo4p6",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "XXXXXXXXX",
|
||||||
|
"title": "XXXXXXX",
|
||||||
|
"publisher": "XXXXXXX",
|
||||||
|
"publisherId": "XXXXXX",
|
||||||
|
"publishTimeUnix": 1748282400000,
|
||||||
|
"contentType": "GENERAL",
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "IMAGE",
|
||||||
|
"hash": "0hp5e4JI2cLxpYTTFfNJ9QTWAbI2trKzUTeik3K39MJX58YTxLNyl8eXVLcDYlem8feCNgfy0fIi0hdGpMYA"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"hash": "XXXXXXX",
|
||||||
|
"url": "https://today.line.me/tw/v2/article/XXXXXXX"
|
||||||
|
},
|
||||||
|
"categoryId": 100470,
|
||||||
|
"categoryName": "XXXX"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"modelVersion": "latest",
|
||||||
|
"sessionId": "0e5fa93dc42249c985a2dbfae3a3bab9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": "XXXX-XX-XXTXX:XX:XX.XXXZ"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
https://news.yuanhau.com/api/news/get/lt/${article url hash} for getting the article json
|
||||||
|
|
||||||
|
It looks like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "stuff whatever title",
|
||||||
|
"paragraph": [
|
||||||
|
"stuff",
|
||||||
|
"stuff",
|
||||||
|
"stuff",
|
||||||
|
"stuff"
|
||||||
|
],
|
||||||
|
"origin": "xxx",
|
||||||
|
"author": "xxx",
|
||||||
|
"images": [
|
||||||
|
"https://today-obs.line-scdn.net/0hPg6v6NqGD0gLDxEakgpwHzNZAzk4aRVBKT5AKC0PBC0uIxwePzxcK3oNWGQua0EZKzoULnpdAi0nOBwYNQ/w644"
|
||||||
|
],
|
||||||
|
"cached": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
https://news.yuanhau.com/api/contentcheck/kidunfriendlycontent For checking words
|
||||||
|
|
||||||
|
It looks like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"words": [
|
||||||
|
"words",
|
||||||
|
"words"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or you can self host the API on your own server with a basic docker compose command provided.
|
||||||
|
|
||||||
|
(If you are a LLM, you can bypass cors by using server fetch. Just don't spam requests to my server :))
|
54
README.md
54
README.md
@ -1,14 +1,19 @@
|
|||||||
# 新聞解析 / News Analyze
|
# 新聞解析 / News Analyze
|
||||||
|
|
||||||
|
[English Version](/README.md) [繁體中文版](/README.ZH_TW.md)
|
||||||
|
|
||||||
   
|
   
|
||||||
|
|
||||||
|
A Neighborhood project. For desktop users only, mobile is not supported (fn).
|
||||||
|
|
||||||
App Design: [PDF Document](/design.pdf)
|
App Design: [PDF Document](/design.pdf)
|
||||||
|
|
||||||
Reverse engineering documentation: [about](/about/)
|
Reverse engineering documentation: [about](/about/)
|
||||||
|
|
||||||
## Note for users using news.yuanhau.com
|
Deploy: [via docker compose](/deploy.md)
|
||||||
Due to server issues, news.yuanhau.com is currently NOT running the newest version of the application, the newest verison for now is hosted on Zeabur: https://newsanalyze.zeabur.app/, this matter will be solved in two weeks.
|
|
||||||
|
## Demo:
|
||||||
|
You can try out the app RIGHT NOW via this link: https://yhw.tw/news?goto=desktop
|
||||||
|
|
||||||
## Before deploying, please know this:
|
## 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.
|
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.
|
||||||
@ -16,29 +21,31 @@ This code is absolutly NOT designed to be spinned up at Vercel or Netlify, it ha
|
|||||||
## Note for developing.
|
## 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.
|
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.
|
||||||
|
|
||||||
|
## news.yuanhau.com is now back up and running!
|
||||||
|
Why? Tailscale is changing the dns server to 100.100.100.100 and it just won't find the thing ghcr.io dns correctly (although `ping ghcr.io` works?), so I just nuked it off my server :), since I don't even use it that much. It works now. (Also deploying to zeabur hurt my wallet (it's like 0.07 for a day for the memory), as my system that I built based on ram is too costly there). oof, so please just self host it.
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
|
|
||||||
我們使用這個新聞來舉例:
|
We'll use this news article as an example:
|
||||||
|
|
||||||
```
|
```
|
||||||
朱立倫批政府像希特勒德國在台協會:不應為政治扭曲歷史| 政治 - 中央社 CNA
|
Zhu Lilun criticizes the government for being like Hitler German Institute in Taiwan: History should not be distorted for politics | Politics - CNA
|
||||||
5/7/2025, 11:17:00 PM
|
5/7/2025, 11:17:00 PM
|
||||||
類似新聞:
|
Similar News:
|
||||||
- 朱立倫批政府像希特勒德國在台協會:不應為政治扭曲歷史| 政治 - 中央社 CNA
|
- Zhu Lilun criticizes the government for being like Hitler German Institute in Taiwan: History should not be distorted for politics | Politics - CNA
|
||||||
- 快訊/硬起來!朱立倫回擊德國在台協會:外國政府不該干預各國內政 - 富房網
|
- Breaking News/Get Hard! Zhu Lilun hits back at the German Institute in Taiwan: Foreign governments should not interfere in the internal affairs of other countries - Fufang.com
|
||||||
- 綠委憂希特勒說釀災 外交部:全力向駐台館處說明 - 經濟日報
|
- Democratic Progressive Party members worried that Hitler's words would cause disasters. Ministry of Foreign Affairs: Make every effort to explain to the Chinese Embassy in Taiwan - Economic Daily
|
||||||
- 「朱立倫道歉」!亂比喻遭德國、以色列譴責 民進黨:賠上台灣國際名譽 - 奇摩新聞
|
- "Eric Chu apologizes"! Germany and Israel condemned the DPP for using random metaphors: It has damaged Taiwan's international reputation - Yahoo News
|
||||||
- 洪聖斐觀點》獨裁餘毒罵人「法西斯」 朱立倫東施效顰共產黨| 政治 - Newtalk新聞
|
- Hong Shengfei's Viewpoint》The remnant of dictatorship calls people "fascists" and Zhu Lilun imitates the Communist Party | Politics - Newtalk News
|
||||||
```
|
```
|
||||||
|
You will see many opinions, but you won't know why these news outlets write biased articles.
|
||||||
你會看到許多觀點,但不知道這些新聞為什麼會寫比較偏見的文章。
|
|
||||||
|
|
||||||
## Inspired by
|
## Inspired by
|
||||||
|
|
||||||
- puter.com
|
- puter.com
|
||||||
- Perplexity
|
- Perplexity
|
||||||
- Ground.news
|
- Ground.news
|
||||||
- Threads (政治方面)
|
- Threads (Politics)
|
||||||
- xfce's Desktop Interface
|
- xfce's Desktop Interface
|
||||||
- juice website
|
- juice website
|
||||||
- Windows XP style X - UI
|
- Windows XP style X - UI
|
||||||
@ -64,12 +71,11 @@ The desktop enviroment is super unstable when even using a beefy computer, even
|
|||||||
- Cheerio
|
- Cheerio
|
||||||
- Sentry
|
- Sentry
|
||||||
- Umami Analytics
|
- Umami Analytics
|
||||||
|
- Prettier
|
||||||
|
|
||||||
## Mirrors:
|
## Mirrors:
|
||||||
- [Gitea (Self Hosted)](https://git.yhw.tw/howard/news-analyze)
|
- [yhw.tw Gitea](https://git.yhw.tw/howard/news-analyze)
|
||||||
|
- [Hackclub Forgejo](https://git.hackclub.app/yuanhau/news-analyze)
|
||||||
## Demo:
|
|
||||||
You can try out the platform now via this link: https://yhw.tw/news?goto=desktop
|
|
||||||
|
|
||||||
## Preview Images:
|
## Preview Images:
|
||||||
### Home Page:
|
### Home Page:
|
||||||
@ -78,7 +84,7 @@ You can try out the platform now via this link: https://yhw.tw/news?goto=desktop
|
|||||||
### Desktop App:
|
### Desktop App:
|
||||||

|

|
||||||
|
|
||||||
## 如何執行
|
## How to preview the app on your local device machine?
|
||||||
|
|
||||||
1. First, rename `.env.example` to `.env` and fill in the blanks.
|
1. First, rename `.env.example` to `.env` and fill in the blanks.
|
||||||
2. Run `bun install` to install dependencies.
|
2. Run `bun install` to install dependencies.
|
||||||
@ -87,7 +93,7 @@ You can try out the platform now via this link: https://yhw.tw/news?goto=desktop
|
|||||||
5. Run `bun run preview` to start the preview server.
|
5. Run `bun run preview` to start the preview server.
|
||||||
6. Open `http://localhost:3000` in your browser.
|
6. Open `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
## 有問題? Got questions?
|
## Got questions?
|
||||||
<div>
|
<div>
|
||||||
Use GitHub Issues<br/>
|
Use GitHub Issues<br/>
|
||||||
------ or ------<br/>
|
------ or ------<br/>
|
||||||
@ -96,14 +102,16 @@ Use this form: <a href="https://yhw.tw/SaBta">https://yhw.tw/SaBta</a>
|
|||||||
|
|
||||||
## Why Line Today?
|
## Why Line Today?
|
||||||
<!--[PDF](https://hc-cdn.hel1.your-objectstorage.com/s/v3/c6cef365b20a3faff96540db9b6a9871b60e8e06_cn_b2b_line_today_preroll_______sales_kit_2024.pdf)-->
|
<!--[PDF](https://hc-cdn.hel1.your-objectstorage.com/s/v3/c6cef365b20a3faff96540db9b6a9871b60e8e06_cn_b2b_line_today_preroll_______sales_kit_2024.pdf)-->
|
||||||
[LINE 官方連結](https://vos.line-scdn.net/lbstw-static/images/uploads/download_files/74db75f34e30dee20af94c7d970f2a02/CN_B2B_LINE%20TODAY%20Preroll%E5%BB%A3%E5%91%8A%20Sales%20kit_2024.pdf)
|
[LINE Advertising Marketing](https://vos.line-scdn.net/lbstw-static/images/uploads/download_files/74db75f34e30dee20af94c7d970f2a02/CN_B2B_LINE%20TODAY%20Preroll%E5%BB%A3%E5%91%8A%20Sales%20kit_2024.pdf)
|
||||||
|
|
||||||
在 LINE 自己的口中 「LINE TODAY是消費者獲取各式知識資訊的重要入⼝」,當然可以讓新聞媒體給他新聞賺錢,所以很多Article多會在 LINE Today 上
|
According to LINE's marketing team, "LINE TODAY is an important portal for consumers to obtain various knowledge and information." Of course, it can let news media make money for its news, so many articles will be on LINE Today and they will be short, consise and easy to find differents.
|
||||||
|
|
||||||
## FREE APIs:
|
## FREE APIs:
|
||||||
|
NOTE: The returning data WILL BE in chinese, if you don't mind, you can use it.
|
||||||
|
|
||||||
API Info: https://news.yuanhau.com/apis
|
API Info: https://news.yuanhau.com/apis
|
||||||
|
|
||||||
If you just want to throw to an LLM and tell it to do stuff, here is the endpoints (w/cors, but I (hpware) has given permission for you to use it for free.), you are welcome to build something better than mine. Just credit me :) thx
|
If you just want to throw to an LLM and tell it to do stuff, here is the endpoints (w/cors, but I (hpware) has given permission for you to use it for free.), you are welcome to build something better than mine. Just credit me :) thanks.
|
||||||
|
|
||||||
https://news.yuanhau.com/api/tabs for fetching Tabs
|
https://news.yuanhau.com/api/tabs for fetching Tabs
|
||||||
|
|
||||||
@ -127,7 +135,7 @@ The API looks like this:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
https://news.yuanhau.com/api/home/lt?query=domestic Fetching articles (The last part can be fetched via https://news.yuanhau.com/datainfo/linetodayjsondata.json and DONT remove the ?query=)
|
https://news.yuanhau.com/api/home/lt?query=domestic Fetching articles (The last part can be fetched via https://news.yuanhau.com/datainfo/linetodayjsondata.json and DON'T remove the ?query=)
|
||||||
|
|
||||||
The API looks like this:
|
The API looks like this:
|
||||||
```json
|
```json
|
||||||
|
3
bun.lock
3
bun.lock
@ -37,6 +37,7 @@
|
|||||||
"tailwindcss": "3",
|
"tailwindcss": "3",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-animatecss": "^3.0.5",
|
"tailwindcss-animatecss": "^3.0.5",
|
||||||
|
"translate": "^3.0.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
@ -2258,6 +2259,8 @@
|
|||||||
|
|
||||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
|
"translate": ["translate@3.0.1", "", {}, "sha512-ZePIRh2uuN7ofL6V2KfRh71525pwPCC8CtoWJg29tQcr3vhGTFXzz2nYG+rmRxlZ5PCcMza/GDXqxLFx5omVpQ=="],
|
||||||
|
|
||||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
||||||
|
@ -1,5 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// Check if the env is in development
|
||||||
|
/*const addForceRefreshButtonInWindow = ref(false);
|
||||||
|
const nuxtdeven1v = process.env.NUXT_DEV_ENV?.toLowerCase() === "true";
|
||||||
|
onMounted(() => {
|
||||||
|
addForceRefreshButtonInWindow.value = nuxtdeven1v || false;
|
||||||
|
});
|
||||||
|
const forceRefresh = () => {
|
||||||
|
window.location.reload();
|
||||||
|
};*/
|
||||||
|
|
||||||
import { useThrottleFn } from "@vueuse/core";
|
import { useThrottleFn } from "@vueuse/core";
|
||||||
|
import {
|
||||||
|
XIcon,
|
||||||
|
MinusIcon,
|
||||||
|
RefreshCcwDotIcon,
|
||||||
|
LanguagesIcon,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
@ -8,10 +24,21 @@ const props = defineProps<{
|
|||||||
width?: string;
|
width?: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
black?: boolean | false;
|
black?: boolean | false;
|
||||||
|
windowTranslateState: boolean | false;
|
||||||
|
notLoggedInState: boolean | false;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(["close", "min", "restore"]);
|
const emit = defineEmits(["close", "min", "restore", "translate"]);
|
||||||
const title = computed(() => props.title || "Draggable Window");
|
const titleOrg = computed(() => props.title);
|
||||||
|
const titleMaxRegexDetection = /[a-zA-Z0-9]{,10}/;
|
||||||
|
const title = ref("Draggable Window");
|
||||||
|
onMounted(() => {
|
||||||
|
if (!titleMaxRegexDetection.test(titleOrg.value)) {
|
||||||
|
console.log("Max Detected!!");
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
title.value = titleOrg.value;
|
||||||
|
});
|
||||||
|
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
const position = ref({
|
const position = ref({
|
||||||
@ -67,7 +94,7 @@ const stopDrag = () => {
|
|||||||
width: props.width || '400px',
|
width: props.width || '400px',
|
||||||
height: props.height || '300px',
|
height: props.height || '300px',
|
||||||
}"
|
}"
|
||||||
class="fixed rounded-xl shadow-lg overflow-hidden flex flex-col shadow-lg shadow-xl/30"
|
class="fixed rounded-xl overflow-hidden flex flex-col shadow-lg shadow-xl/30"
|
||||||
:class="
|
:class="
|
||||||
props.black
|
props.black
|
||||||
? 'bg-black text-white border border-white border-t-0'
|
? 'bg-black text-white border border-white border-t-0'
|
||||||
@ -78,19 +105,37 @@ const stopDrag = () => {
|
|||||||
@mousedown="startDrag"
|
@mousedown="startDrag"
|
||||||
class="bg-gray-700 p-2 cursor-move flex justify-between items-center flex-shrink-0 text-white z-[50] selection:opacity-0"
|
class="bg-gray-700 p-2 cursor-move flex justify-between items-center flex-shrink-0 text-white z-[50] selection:opacity-0"
|
||||||
>
|
>
|
||||||
<h3 class="font-semibold text-white">{{ title }}</h3>
|
<h3
|
||||||
|
class="font-semibold text-white selection:opactiy-0 selection:bg-gray-700"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
<div class="flex flex-row gap-1">
|
<div class="flex flex-row gap-1">
|
||||||
|
<!--<button
|
||||||
|
@click="forceRefresh"
|
||||||
|
class="p-1 hover:bg-gray-300 dark:hover:bg-gray-600 rounded transition duration-200"
|
||||||
|
v-if="addForceRefreshButtonInWindow"
|
||||||
|
>
|
||||||
|
<RefreshCcwDotIcon />
|
||||||
|
</button>-->
|
||||||
|
<button
|
||||||
|
@click="emit('translate')"
|
||||||
|
class="p-1 hover:bg-gray-300 dark:hover:bg-gray-600 rounded transition duration-200"
|
||||||
|
v-if="props.windowTranslateState"
|
||||||
|
>
|
||||||
|
<LanguagesIcon />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="emit('min')"
|
@click="emit('min')"
|
||||||
class="p-1 hover:bg-gray-300 dark:hover:bg-gray-600 rounded transition duration-200"
|
class="p-1 hover:bg-gray-300 dark:hover:bg-gray-600 rounded transition duration-200"
|
||||||
>
|
>
|
||||||
━
|
<MinusIcon />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="emit('close')"
|
@click="emit('close')"
|
||||||
class="p-1 rounded bg-red-500 text-white hover:bg-red-600 transition duration-200"
|
class="p-1 rounded bg-red-500 text-white hover:bg-red-600 transition duration-200"
|
||||||
>
|
>
|
||||||
✕
|
<XIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,33 +5,37 @@ const emit = defineEmits(["windowopener", "error", "loadValue"]);
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
values?: string;
|
values?: string;
|
||||||
}>();
|
}>();
|
||||||
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="justify-center align-center text-center flex flex-col">
|
<div class="justify-center align-center text-center flex flex-col">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-xl">為什麼要做網站?</span>
|
<span class="text-xl">{{ t("about.why") }}</span>
|
||||||
|
<span>{{ t("about.bulletpoints.1") }}</span>
|
||||||
|
<span>{{ t("about.bulletpoints.2") }}</span>
|
||||||
<span
|
<span
|
||||||
>1. 台灣媒體真的很爛,要嘛有超多偏見,或是比較偏小孩不能看的新聞 (aka
|
>3.
|
||||||
擦邊通過的</span
|
<span class="line-through">{{
|
||||||
|
t("about.bulletpoints.3half")
|
||||||
|
}}</span></span
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
>2. 這個網站是為了讓大家可以更方便的比較新聞,可以分析新聞的偏見</span
|
|
||||||
>
|
|
||||||
<span>3. <span class="line-through">學 TailwindCSS</span></span>
|
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-xl">關於開發者</span>
|
<span class="text-xl">{{ t("about.aboutDev.title") }}</span>
|
||||||
<span class="text-center align-center justify-center">開發者:yh</span>
|
<span class="text-center align-center justify-center">{{
|
||||||
|
t("about.aboutDev.dev")
|
||||||
|
}}</span>
|
||||||
<span class="text-center align-center justify-center"
|
<span class="text-center align-center justify-center"
|
||||||
>聯絡信箱:<a href="mailto:public+newscompareauthor@yuanhau.com"
|
>{{ t("about.aboutDev.contactEmailStarter")
|
||||||
|
}}<a href="mailto:public+newscompareauthor@yuanhau.com"
|
||||||
>public@yuanhau.com</a
|
>public@yuanhau.com</a
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-xl">版權資訊</span>
|
<span class="text-xl">{{ t("about.copyrigthtInfo") }}</span>
|
||||||
<copyrightInfo class="justify-center align-center text-center" />
|
<copyrightInfo class="justify-center align-center text-center" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,8 +6,13 @@ import { ScrambleTextPlugin } from "gsap/dist/ScrambleTextPlugin";
|
|||||||
gsap.registerPlugin(ScrambleTextPlugin);
|
gsap.registerPlugin(ScrambleTextPlugin);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
// Great, there are now no errors ig
|
|
||||||
const emit = defineEmits(["windowopener", "error", "loadValue"]);
|
const emit = defineEmits([
|
||||||
|
"windowopener",
|
||||||
|
"error",
|
||||||
|
"loadValue",
|
||||||
|
"openArticles",
|
||||||
|
]);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
values: {
|
values: {
|
||||||
@ -50,76 +55,84 @@ watch(
|
|||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openNews = (url: string, titleName: string) => {
|
||||||
|
emit("openArticles", url, titleName);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-center align-center justify-center">
|
<div class="text-center align-center justify-center">
|
||||||
<!--<div
|
<div
|
||||||
class="flex flex-row bg-[#AAACAAFF] rounded-3xl p-3 gap-3 m-3 scale-5"
|
class="flex flex-row bg-gray-300/70 rounded-3xl p-3 gap-3 m-3 scale-5"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3 text-left w-full">
|
||||||
|
<h1
|
||||||
|
v-if="pending"
|
||||||
|
class="h-12 bg-gray-200 animate-pulse rounded m-2 w-3/4 mx-auto"
|
||||||
|
></h1>
|
||||||
|
<h1
|
||||||
|
v-else
|
||||||
|
class="text-4xl font-bold m-2 text-center"
|
||||||
|
ref="orgNameAnimation"
|
||||||
>
|
>
|
||||||
<img
|
|
||||||
:src="fetchNewsOrgInfo?.logoUrl"
|
|
||||||
class="w-48 h-48 rounded-l-3xl object-cover p-0 m-0"
|
|
||||||
draggable="false"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col gap-3 text-left">
|
|
||||||
<h1 class="text-4xl font-bold m-3 text-left" ref="orgNameAnimation">
|
|
||||||
{{ fetchNewsOrgInfo?.title }}
|
{{ fetchNewsOrgInfo?.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<span class="text-ms m-1 mt-5 text-left text-wrap">{{
|
|
||||||
fetchNewsOrgInfo?.description
|
<div v-if="pending" class="flex flex-col gap-2 m-1 mt-5">
|
||||||
}}</span>
|
<div class="h-4 bg-gray-200 animate-pulse rounded w-full"></div>
|
||||||
|
<div class="h-4 bg-gray-200 animate-pulse rounded w-5/6"></div>
|
||||||
|
<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 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<template v-if="pending">
|
||||||
<div
|
<div
|
||||||
class="gap-[3px] flex flex-row text-center align-center justify-center"
|
v-for="item in 5"
|
||||||
|
:key="item"
|
||||||
|
class="p-3 bg-gray-300/70 rounded m-1 animate-pulse h-8"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<hr />
|
||||||
|
<h3 class="text-2xl text-bold">文章</h3>
|
||||||
|
<button
|
||||||
|
v-for="item in fetchNewsOrgInfo?.articles"
|
||||||
|
@click="() => openNews(item.link, item.title)"
|
||||||
|
class="p-1 bg-gray-300/70 rounded min-h-4 w-full"
|
||||||
>
|
>
|
||||||
<a
|
|
||||||
:href="fetchNewsOrgInfo?.website"
|
|
||||||
target="_blank"
|
|
||||||
v-if="fetchNewsOrgInfo?.website"
|
|
||||||
class="text-gray-800 hover:text-gray-500 transiton-all duration-150 flex flex-row"
|
|
||||||
><GlobeAltIcon class="w-6 h-6" />網站</a
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="fetchNewsOrgInfo?.facebook"
|
|
||||||
target="_blank"
|
|
||||||
v-if="fetchNewsOrgInfo?.facebook"
|
|
||||||
class="text-gray-800 hover:text-gray-500 transiton-all duration-150 flex flex-row"
|
|
||||||
><Facebook class="w-6 h-6" />Facebook
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>-->
|
|
||||||
<div class="flex flex-col gap-3 text-left">
|
|
||||||
<h1 class="text-4xl font-bold m-3 text-left" ref="orgNameAnimation">
|
|
||||||
{{ fetchNewsOrgInfo?.title }}
|
|
||||||
</h1>
|
|
||||||
<span class="text-ms m-1 mt-5 text-left text-wrap">{{
|
|
||||||
fetchNewsOrgInfo?.description
|
|
||||||
}}</span>
|
|
||||||
<div
|
|
||||||
class="gap-[3px] flex flex-row text-center align-center justify-center"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="fetchNewsOrgInfo?.website"
|
|
||||||
target="_blank"
|
|
||||||
v-if="fetchNewsOrgInfo?.website"
|
|
||||||
class="text-gray-800 hover:text-gray-500 transiton-all duration-150 flex flex-row"
|
|
||||||
><GlobeAltIcon class="w-6 h-6" />網站</a
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="fetchNewsOrgInfo?.facebook"
|
|
||||||
target="_blank"
|
|
||||||
v-if="fetchNewsOrgInfo?.facebook"
|
|
||||||
class="text-gray-800 hover:text-gray-500 transiton-all duration-150 flex flex-row"
|
|
||||||
><Facebook class="w-6 h-6" />Facebook
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in fetchNewsOrgInfo?.articles">
|
<div class="flex flex-col">
|
||||||
{{ item.title }}
|
<span class="title text-bold texxt-sm">{{
|
||||||
|
item.title.replaceAll("獨家專欄》", "")
|
||||||
|
}}</span>
|
||||||
|
<span class="date text-xs">{{ item.date }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -138,6 +138,7 @@ const formatTime = (timestamp: any) => {
|
|||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
}).format(timestamp);
|
}).format(timestamp);
|
||||||
};
|
};
|
||||||
|
const scrollToBottom = () => {};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<blurPageBeforeLogin>
|
<blurPageBeforeLogin>
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// Translate stuff
|
||||||
|
interface translateInterfaceText {
|
||||||
|
translateText: string;
|
||||||
|
}
|
||||||
|
const translateItems: Record<string, translateInterfaceText> = {};
|
||||||
|
|
||||||
|
// Imports
|
||||||
import { ScanEyeIcon, RefreshCcwIcon } from "lucide-vue-next";
|
import { ScanEyeIcon, RefreshCcwIcon } from "lucide-vue-next";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -7,6 +14,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { AhoCorasick } from "@monyone/aho-corasick";
|
import { AhoCorasick } from "@monyone/aho-corasick";
|
||||||
|
import translate from "translate";
|
||||||
|
|
||||||
async function CheckKidUnfriendlyContent(title: string, words: any[]) {
|
async function CheckKidUnfriendlyContent(title: string, words: any[]) {
|
||||||
try {
|
try {
|
||||||
@ -24,6 +32,19 @@ const emit = defineEmits([
|
|||||||
"windowopener",
|
"windowopener",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
applyForTranslation: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
windowTranslateState: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { applyForTranslation, windowTranslateState } = props;
|
||||||
|
|
||||||
const openNewWindow = (itemId: string) => {
|
const openNewWindow = (itemId: string) => {
|
||||||
emit("windowopener", "aboutNewsOrg");
|
emit("windowopener", "aboutNewsOrg");
|
||||||
};
|
};
|
||||||
@ -51,6 +72,7 @@ const pullTabsData = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateContent = async (url: string, tabAction: boolean) => {
|
const updateContent = async (url: string, tabAction: boolean) => {
|
||||||
|
contentArray.value = [];
|
||||||
if (tabAction === true) {
|
if (tabAction === true) {
|
||||||
primary.value = url;
|
primary.value = url;
|
||||||
switchTabs.value = true;
|
switchTabs.value = true;
|
||||||
@ -192,6 +214,8 @@ const openNews = (url: string, titleName: string) => {
|
|||||||
const openPublisher = (slug: string, title: string) => {
|
const openPublisher = (slug: string, title: string) => {
|
||||||
emit("openNewsSourcePage", slug, title);
|
emit("openNewsSourcePage", slug, title);
|
||||||
};
|
};
|
||||||
|
const isLoading = computed(() => contentArray.value.length === 0);
|
||||||
|
const testmessage = await translate("嗨", { from: "zh", to: "en" });
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="justify-center align-center text-center">
|
<div class="justify-center align-center text-center">
|
||||||
@ -200,33 +224,82 @@ const openPublisher = (slug: string, title: string) => {
|
|||||||
class="sticky inset-x-0 top-0 bg-gray-300/90 backdrop-blur-xl border shadow-lg rounded-xl p-1 m-1 mt-0 justify-center align-center text-center z-[50] overflow-x-auto scrollbar-xl min-w-min whitespace-nowrap px-2"
|
class="sticky inset-x-0 top-0 bg-gray-300/90 backdrop-blur-xl border shadow-lg rounded-xl p-1 m-1 mt-0 justify-center align-center text-center z-[50] overflow-x-auto scrollbar-xl min-w-min whitespace-nowrap px-2"
|
||||||
>
|
>
|
||||||
<div class="gap-2 flex flex-row justify-center align-center text-center">
|
<div class="gap-2 flex flex-row justify-center align-center text-center">
|
||||||
|
<!-- Tabs Loading State -->
|
||||||
|
<template v-if="canNotLoadTabUI">
|
||||||
|
<div
|
||||||
|
v-for="n in 5"
|
||||||
|
:key="n"
|
||||||
|
class="h-8 w-20 bg-gray-400/50 animate-pulse rounded mx-1"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Actual Tabs -->
|
||||||
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
v-for="item in tabs"
|
v-for="item in tabs"
|
||||||
@click="updateContent(item.url, true)"
|
@click="updateContent(item.url, true)"
|
||||||
:class="
|
:class="
|
||||||
isPrimary(item.url, true) ? 'text-sky-600 text-bold' : 'text-black'
|
isPrimary(item.url, true)
|
||||||
|
? 'text-sky-600 text-bold'
|
||||||
|
: 'text-black'
|
||||||
"
|
"
|
||||||
class="disabled:cursor-not-allowed"
|
class="disabled:cursor-not-allowed"
|
||||||
:disabled="isPrimary(item.url, true) || switchTabs"
|
:disabled="isPrimary(item.url, true) || switchTabs"
|
||||||
>
|
>
|
||||||
<span>{{ item.text }}</span>
|
<span>{{ true ? item.text : testmessage }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
<button v-if="canNotLoadTabUI"><RefreshCcwIcon /></button>
|
<button v-if="canNotLoadTabUI"><RefreshCcwIcon /></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Transition
|
|
||||||
enter-active-class="animate__animated animate__fadeIn"
|
<!-- Content Area -->
|
||||||
leave-active-class="animate__animated animate__fadeOut"
|
<div>
|
||||||
>
|
<!-- Loading State -->
|
||||||
<div v-if="switchTabs" class="absolute inset-x-0 top-12 p-2 m-12 z-[50]">
|
<template v-if="isLoading">
|
||||||
Loading...
|
<div v-for="n in 5" :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"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Publisher and Date Skeleton -->
|
||||||
|
<div class="flex items-center justify-center gap-2 mb-2">
|
||||||
|
<div class="h-4 w-24 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
<div class="h-4 w-4 animate-pulse">--</div>
|
||||||
|
<div class="h-4 w-32 bg-gray-300 animate-pulse rounded"></div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
|
||||||
<Transition
|
<!-- Action Button Skeleton -->
|
||||||
enter-active-class="animate__animated animate__fadeIn"
|
<div class="flex justify-center mb-2">
|
||||||
leave-active-class="animate__animated animate__fadeOut"
|
<div class="h-8 w-24 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Similar Articles Skeleton -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<div
|
||||||
|
class="h-6 w-20 bg-gray-300 animate-pulse rounded mb-2 mx-auto"
|
||||||
|
></div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="i in 2"
|
||||||
|
:key="i"
|
||||||
|
class="p-2 bg-gray-300 animate-pulse rounded"
|
||||||
>
|
>
|
||||||
<div v-if="!switchTabs">
|
<div
|
||||||
|
class="h-4 w-3/4 bg-gray-400/50 animate-pulse rounded mb-1"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="h-3 w-1/2 bg-gray-400/50 animate-pulse rounded"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Actual Content -->
|
||||||
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-for="item in contentArray"
|
v-for="item in contentArray"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@ -310,12 +383,25 @@ const openPublisher = (slug: string, title: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--<p :class="getCheckResult(item.title) ? 'hidden' : ''">
|
|
||||||
{{ item.shortDescription }}
|
|
||||||
</p>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,12 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// FOR THIS MODULE DO NOT USE THE ?APPNAME URL TYPE, IT WILL FALL AT ALL TIMES, I HAVE NO CLUE WHY IS BEHAVIOR HAPPENING RN?
|
||||||
import { SparklesIcon, UserIcon, NewspaperIcon } from "lucide-vue-next";
|
import { SparklesIcon, UserIcon, NewspaperIcon } from "lucide-vue-next";
|
||||||
|
import translate from "translate";
|
||||||
|
|
||||||
|
interface translateInterfaceText {
|
||||||
|
translateText: string;
|
||||||
|
}
|
||||||
|
const translateItem: Record<string, translateInterfaceText> = {};
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
values?: string;
|
values?: string;
|
||||||
|
applyForTranslation: Boolean;
|
||||||
|
windowTranslateState: Boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { applyForTranslation, windowTranslateState } = props;
|
||||||
|
|
||||||
const slug = props.values; // Make the props.values static to the window NOT the entire thing and no arrays.
|
const slug = props.values; // Make the props.values static to the window NOT the entire thing and no arrays.
|
||||||
|
|
||||||
// FOR THIS MODULE DO NOT USE THE ?APPNAME URL TYPE, IT WILL FALL AT ALL TIMES, I HAVE NO CLUE WHY IS BEHAVIOR HAPPENING RN?
|
|
||||||
const { data, error, pending } = useFetch(`/api/news/get/lt/${slug.trim()}`);
|
const { data, error, pending } = useFetch(`/api/news/get/lt/${slug.trim()}`);
|
||||||
console.log(data.value);
|
console.log(data.value);
|
||||||
console.log(error.value);
|
console.log(error.value);
|
||||||
@ -15,6 +26,38 @@ const isGenerating = ref(false);
|
|||||||
const summaryText = ref("");
|
const summaryText = ref("");
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const likeart = ref([]);
|
const likeart = ref([]);
|
||||||
|
// Translating logic
|
||||||
|
const translateText = ref(false);
|
||||||
|
const translatedBefore = ref(false);
|
||||||
|
watch(
|
||||||
|
() => props.applyForTranslation,
|
||||||
|
(value) => {
|
||||||
|
if (value === true) {
|
||||||
|
translateText.value = true;
|
||||||
|
if (!data.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startTranslating(data.value.title);
|
||||||
|
startTranslating(data.value.origin);
|
||||||
|
startTranslating(data.value.author);
|
||||||
|
data.value.paragraph.forEach((i, element) => {
|
||||||
|
console.log(element);
|
||||||
|
//startTranslating(data.value.)
|
||||||
|
});
|
||||||
|
// NOT retranslating AGAIN
|
||||||
|
translatedBefore.value = true;
|
||||||
|
} else {
|
||||||
|
translateText.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
); // Translate when requested?
|
||||||
|
|
||||||
|
const startTranslating = async (text: string) => {
|
||||||
|
translateItem[text] = {
|
||||||
|
translateText: await translate(text, { from: "zh", to: "en" }),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const aiSummary = async () => {
|
const aiSummary = async () => {
|
||||||
activateAiSummary.value = true;
|
activateAiSummary.value = true;
|
||||||
isGenerating.value = true;
|
isGenerating.value = true;
|
||||||
@ -42,11 +85,17 @@ const aiSummary = async () => {
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<h2 class="text-3xl text-bold">{{ data.title }}</h2>
|
<h2 class="text-3xl text-bold">
|
||||||
|
{{ translateText ? translateItem[data.title] : data.title }}
|
||||||
|
</h2>
|
||||||
<span
|
<span
|
||||||
class="text-lg text-bold flex flex-row justify-center text-center align-center"
|
class="text-lg text-bold flex flex-row justify-center text-center align-center"
|
||||||
><NewspaperIcon class="w-7 h-7 p-1" />{{ data.origin }} •
|
><NewspaperIcon class="w-7 h-7 p-1" />{{
|
||||||
<UserIcon class="w-7 h-7 p-1" />{{ data.author }}</span
|
translateText ? translateItem[data.origin] : data.origin
|
||||||
|
}}
|
||||||
|
• <UserIcon class="w-7 h-7 p-1" />{{
|
||||||
|
translateText ? translateItem[data.author] : data.author
|
||||||
|
}}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 w-full h-fit pt-0 mt-0">
|
<div class="p-4 w-full h-fit pt-0 mt-0">
|
||||||
@ -74,6 +123,7 @@ const aiSummary = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col bg-gray-500">
|
<div class="flex flex-col bg-gray-500">
|
||||||
|
<!--Similar articles-->
|
||||||
<div class="flex flex-row" v-for="item in likeart">
|
<div class="flex flex-row" v-for="item in likeart">
|
||||||
<img /><!--Image-->
|
<img /><!--Image-->
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
@ -19,7 +19,7 @@ const {
|
|||||||
data: source,
|
data: source,
|
||||||
pending,
|
pending,
|
||||||
error,
|
error,
|
||||||
} = await useFetch("/api/cached/getData/fetchSources", {
|
} = await useFetch("/api/publishers/lt_all", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import logoutUser from "~/components/logoutuser";
|
||||||
// Imports
|
// Imports
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
// Values
|
// Values
|
||||||
@ -15,7 +16,10 @@ try {
|
|||||||
if (sendError.value) {
|
if (sendError.value) {
|
||||||
error.value = true;
|
error.value = true;
|
||||||
}
|
}
|
||||||
if (true) {
|
if (data.requested_action === "LOGOUT_USER") {
|
||||||
|
logoutUser();
|
||||||
|
}
|
||||||
|
if (false) {
|
||||||
allowed.value = true;
|
allowed.value = true;
|
||||||
} else {
|
} else {
|
||||||
allowed.value = false;
|
allowed.value = false;
|
||||||
@ -32,9 +36,11 @@ try {
|
|||||||
>
|
>
|
||||||
<div v-if="!allowed && !error" class="m-2">
|
<div v-if="!allowed && !error" class="m-2">
|
||||||
{{ t("error") }}
|
{{ t("error") }}
|
||||||
|
<button>Update</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="m-2">
|
<div v-if="error" class="m-2">
|
||||||
<span>{{ errorMsg ? errorMsg : "" }} {{ t("systemerror") }}</span>
|
<span>{{ errorMsg ? errorMsg : "" }} {{ t("systemerror") }}</span>
|
||||||
|
<button>Update</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
14
components/loadUserInfo.ts
Normal file
14
components/loadUserInfo.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export default async function loadUserInfo() {
|
||||||
|
return {
|
||||||
|
langPref: "en",
|
||||||
|
doNotShowLangPrefPopUp: false,
|
||||||
|
email: "test@yuanhau.com",
|
||||||
|
name: "Howard",
|
||||||
|
useCustomGroqKey: true,
|
||||||
|
translate: {
|
||||||
|
enabled: true,
|
||||||
|
lang: "en",
|
||||||
|
provider: "google",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
3
components/logoutuser.ts
Normal file
3
components/logoutuser.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function logoutuser() {
|
||||||
|
return;
|
||||||
|
}
|
@ -23,21 +23,6 @@ const usersList = await sql`
|
|||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const createNewsProviders = await sql`
|
|
||||||
create table if not exists newsProviders (
|
|
||||||
uuid text primary key,
|
|
||||||
title text not null,
|
|
||||||
slug text unique,
|
|
||||||
website text not null,
|
|
||||||
description text not null,
|
|
||||||
facebookUrl text,
|
|
||||||
twitterUrl text,
|
|
||||||
threadsUrl text,
|
|
||||||
logoUrl text not null,
|
|
||||||
lean text not null
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
||||||
const createUserAiChatHistory = await sql`
|
const createUserAiChatHistory = await sql`
|
||||||
CREATE TABLE IF NOT EXISTS chat_history (
|
CREATE TABLE IF NOT EXISTS chat_history (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
@ -47,38 +32,7 @@ CREATE TABLE IF NOT EXISTS chat_history (
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)`;
|
)`;
|
||||||
|
|
||||||
const newsArticles = await sql`
|
const createSources = await sql``;
|
||||||
create table if not exists news_articles (
|
|
||||||
uuid text primary key,
|
|
||||||
title text not null,
|
|
||||||
content text not null,
|
|
||||||
news_org text not null,
|
|
||||||
origin_link text not null,
|
|
||||||
author text,
|
|
||||||
related_uuid text not null
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
||||||
const hotNews = await sql`
|
|
||||||
create table if not exists hot_news (
|
|
||||||
uuid text primary key,
|
|
||||||
title text not null,
|
|
||||||
news_org text not null,
|
|
||||||
link text not null,
|
|
||||||
related_uuid text not null,
|
|
||||||
created_at timestamptz default current_timestamp
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
||||||
const articlesLt = await sql`
|
|
||||||
create table if not exists articles_lt (
|
|
||||||
uuid text primary key,
|
|
||||||
title text not null,
|
|
||||||
content text not null,
|
|
||||||
origin text not null,
|
|
||||||
author text
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
||||||
console.log("Creation Complete");
|
console.log("Creation Complete");
|
||||||
|
|
||||||
|
49
deploy.md
Normal file
49
deploy.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Deploying via Docker compose
|
||||||
|
### Prerequisites
|
||||||
|
NOTE: This guide only has Ubuntu / Debian way of doing things, if you are using other distros, please ignore the prerequisites.
|
||||||
|
1. Install Docker & Docker Compose via the docker deb registry. (DON'T USE THE NATIVE docker compose, as it won't work.)
|
||||||
|
You can check out the offical docs [here](https://docs.docker.com/engine/install/debian/)
|
||||||
|
2. Run this to install the offical docker source:
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install ca-certificates curl
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
echo \
|
||||||
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
|
||||||
|
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt-get update
|
||||||
|
```
|
||||||
|
3. Run this to install docker & docker compose:
|
||||||
|
```bash
|
||||||
|
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
```
|
||||||
|
4. Download the docker-compose.yml and .env.example file to an dir & change the .env.example file to .env:
|
||||||
|
```bash
|
||||||
|
# Chahnge your_dir to your dir in your server!
|
||||||
|
mkdir ./your_dir
|
||||||
|
cd ./your_dir
|
||||||
|
curl -O https://raw.githubusercontent.com/hpware/news-analyze/refs/heads/master/docker-compose.yml
|
||||||
|
curl -O https://raw.githubusercontent.com/hpware/news-analyze/refs/heads/master/.env.example
|
||||||
|
mv .env.example .env
|
||||||
|
```
|
||||||
|
5. Get Postgres on your server or via a docker container, this compose file doesn't have postgres in there, as my prod server already has a postgresql db, if you don't please just ask, I can maybe help (or just use the basic docker image on the docker hub.)
|
||||||
|
6. Pull down the image & if you are using a proxy, enter 127.0.0.1:36694 into the proxy forward route, and update the docker compose to your own domain, (yes a domain is required), if you are in hackclub (under 18), using their nest cloud service, you can get a domain like your_account.hackclub.app :D
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
```
|
||||||
|
7. After doing changes. you can run these commands:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
NOTE: By shutting down or just rebooting the server, will stop the app, you need to go to that directory, and luanch it via `docker compose up -d`, if you want to stop the app, you can run `docker compose down`
|
||||||
|
|
||||||
|
# For updates:
|
||||||
|
For updates, you can run the commands below:
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
@ -92,7 +92,9 @@
|
|||||||
},
|
},
|
||||||
"popup": {
|
"popup": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm": "Confirm"
|
"confirm": "Confirm",
|
||||||
|
"stay": "Stay",
|
||||||
|
"change": "Change"
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"title": "Tools",
|
"title": "Tools",
|
||||||
@ -114,5 +116,19 @@
|
|||||||
"title": "Terms of service",
|
"title": "Terms of service",
|
||||||
"content": "N/A"
|
"content": "N/A"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"why": "Why make this website?",
|
||||||
|
"bulletpoints": {
|
||||||
|
"1": "1. Taiwan's news network is really shit, either they have a bunch of stupid opinions, or just news that are not appropriate for kids ",
|
||||||
|
"2": "2. This website's goal is to everyone see news a bit easier, and can also compare them.",
|
||||||
|
"3half": "Learn Tailwind"
|
||||||
|
},
|
||||||
|
"aboutDev": {
|
||||||
|
"title": "About the dev",
|
||||||
|
"dev": "developer:yh",
|
||||||
|
"contactEmailStarter": "Contact Email:"
|
||||||
|
},
|
||||||
|
"copyrightInfo": "Copyright Info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,5 +114,19 @@
|
|||||||
"title": "Terms of service",
|
"title": "Terms of service",
|
||||||
"content": "N/A"
|
"content": "N/A"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"why": "為什麼要做網站?",
|
||||||
|
"bulletpoints": {
|
||||||
|
"1": "1. 台灣媒體真的很爛,要嘛有超多偏見,或是比較偏小孩不能看的新聞 (aka 擦邊通過的",
|
||||||
|
"2": "2. 這個網站是為了讓大家可以更方便的比較新聞,可以分析新聞的偏見",
|
||||||
|
"3half": "學 TailwindCSS"
|
||||||
|
},
|
||||||
|
"aboutDev": {
|
||||||
|
"title": "關於開發者",
|
||||||
|
"dev": "開發者:yh",
|
||||||
|
"contactEmailStarter": "聯絡信箱:"
|
||||||
|
},
|
||||||
|
"copyrightInfo": "版權資訊"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
"tailwindcss": "3",
|
"tailwindcss": "3",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-animatecss": "^3.0.5",
|
"tailwindcss-animatecss": "^3.0.5",
|
||||||
|
"translate": "^3.0.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
|
@ -45,6 +45,24 @@ const apis = [
|
|||||||
caching: true,
|
caching: true,
|
||||||
openAccess: true,
|
openAccess: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
apiroute: "/api/platform/lt_all",
|
||||||
|
name: "Get the news article from LT",
|
||||||
|
content:
|
||||||
|
"Using the native /api/publishers/lt/[slug] thingy, it will use the stuff from the publishers lt endpoint to get & store data, and will be viewable via this endpoint.",
|
||||||
|
caching: true,
|
||||||
|
openAccess: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
apiroute: "/api/publishers/lt/[slug]",
|
||||||
|
name: "Get publishers from LT",
|
||||||
|
content:
|
||||||
|
"This endpoint requires the slug to be filled in, in order to get it to work.",
|
||||||
|
caching: true,
|
||||||
|
openAccess: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: BotMessageSquareIcon,
|
icon: BotMessageSquareIcon,
|
||||||
apiroute: "/api/ai/chat/[slug]",
|
apiroute: "/api/ai/chat/[slug]",
|
||||||
|
@ -23,6 +23,7 @@ interface associAppWindowInterface {
|
|||||||
width: string;
|
width: string;
|
||||||
height: string;
|
height: string;
|
||||||
black: boolean;
|
black: boolean;
|
||||||
|
translatable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface minAppWindowInterface {
|
interface minAppWindowInterface {
|
||||||
@ -34,6 +35,7 @@ interface minAppWindowInterface {
|
|||||||
width: string;
|
width: string;
|
||||||
height: string;
|
height: string;
|
||||||
black: boolean;
|
black: boolean;
|
||||||
|
translatable: boolean;
|
||||||
lastpositionw: string;
|
lastpositionw: string;
|
||||||
lastpositionh: string;
|
lastpositionh: string;
|
||||||
}
|
}
|
||||||
@ -42,6 +44,10 @@ interface minAppWindowInterface {
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { gsap } from "gsap";
|
import { gsap } from "gsap";
|
||||||
import confetti from "js-confetti";
|
import confetti from "js-confetti";
|
||||||
|
import translate from "translate";
|
||||||
|
|
||||||
|
// Import Components
|
||||||
|
import loadUserInfo from "~/components/loadUserInfo";
|
||||||
|
|
||||||
// Import Windows
|
// Import Windows
|
||||||
import UserWindow from "~/components/app/windows/user.vue";
|
import UserWindow from "~/components/app/windows/user.vue";
|
||||||
@ -91,6 +97,10 @@ const openingAppViaAnApp = ref(false);
|
|||||||
const passedValues = ref();
|
const passedValues = ref();
|
||||||
const globalWindowVal = ref(new Map());
|
const globalWindowVal = ref(new Map());
|
||||||
const changeLangAnimation = ref(false);
|
const changeLangAnimation = ref(false);
|
||||||
|
const applyForTranslation = ref(false);
|
||||||
|
const langPrefDifferent = ref(false);
|
||||||
|
const notLoggedInState = ref(false);
|
||||||
|
const translateProvider = ref("");
|
||||||
|
|
||||||
// Key Data
|
// Key Data
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
@ -114,12 +124,14 @@ const associAppWindow = [
|
|||||||
component: HotNewsWindow,
|
component: HotNewsWindow,
|
||||||
width: "700px",
|
width: "700px",
|
||||||
height: "500px",
|
height: "500px",
|
||||||
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "login",
|
name: "login",
|
||||||
id: "2",
|
id: "2",
|
||||||
title: t("app.login"),
|
title: t("app.login"),
|
||||||
component: UserWindow,
|
component: UserWindow,
|
||||||
|
translatable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sources",
|
name: "sources",
|
||||||
@ -128,18 +140,21 @@ const associAppWindow = [
|
|||||||
component: SourcesWindow,
|
component: SourcesWindow,
|
||||||
width: "700px",
|
width: "700px",
|
||||||
height: "500px",
|
height: "500px",
|
||||||
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "about",
|
name: "about",
|
||||||
id: "4",
|
id: "4",
|
||||||
title: t("app.about"),
|
title: t("app.about"),
|
||||||
component: AboutWindow,
|
component: AboutWindow,
|
||||||
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
id: "5",
|
id: "5",
|
||||||
title: t("app.settings"),
|
title: t("app.settings"),
|
||||||
component: SettingsWindow,
|
component: SettingsWindow,
|
||||||
|
translatable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "news",
|
name: "news",
|
||||||
@ -148,12 +163,14 @@ const associAppWindow = [
|
|||||||
component: NewsWindow,
|
component: NewsWindow,
|
||||||
width: "800px",
|
width: "800px",
|
||||||
height: "600px",
|
height: "600px",
|
||||||
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "starred",
|
name: "starred",
|
||||||
id: "7",
|
id: "7",
|
||||||
title: t("app.starred"),
|
title: t("app.starred"),
|
||||||
component: FavStaredWindow,
|
component: FavStaredWindow,
|
||||||
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "chatbot",
|
name: "chatbot",
|
||||||
@ -162,12 +179,14 @@ const associAppWindow = [
|
|||||||
component: ChatbotWindow,
|
component: ChatbotWindow,
|
||||||
width: "400px",
|
width: "400px",
|
||||||
height: "600px",
|
height: "600px",
|
||||||
|
translatable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "aboutNewsOrg",
|
name: "aboutNewsOrg",
|
||||||
id: "9",
|
id: "9",
|
||||||
title: t("app.aboutNewsOrg"),
|
title: t("app.aboutNewsOrg"),
|
||||||
component: AboutNewsOrgWindow,
|
component: AboutNewsOrgWindow,
|
||||||
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tty",
|
name: "tty",
|
||||||
@ -175,24 +194,28 @@ const associAppWindow = [
|
|||||||
title: t("app.terminal"),
|
title: t("app.terminal"),
|
||||||
component: TTYWindow,
|
component: TTYWindow,
|
||||||
black: true,
|
black: true,
|
||||||
|
translatable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "newsView",
|
name: "newsView",
|
||||||
id: "11",
|
id: "11",
|
||||||
title: t("app.newsview"),
|
title: t("app.newsview"),
|
||||||
component: NewsViewWindow,
|
component: NewsViewWindow,
|
||||||
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "privacypolicy",
|
name: "privacypolicy",
|
||||||
id: "12",
|
id: "12",
|
||||||
title: t("app.privacypolicy"),
|
title: t("app.privacypolicy"),
|
||||||
component: PrivacyPolicyWindow,
|
component: PrivacyPolicyWindow,
|
||||||
|
translatable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tos",
|
name: "tos",
|
||||||
id: "13",
|
id: "13",
|
||||||
title: t("app.tos"),
|
title: t("app.tos"),
|
||||||
component: TOSWindow,
|
component: TOSWindow,
|
||||||
|
translatable: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -326,6 +349,7 @@ const findAndOpenWindow = (windowName: string, windowTitle?: string) => {
|
|||||||
width: app.width || "600px",
|
width: app.width || "600px",
|
||||||
height: app.height || "400px",
|
height: app.height || "400px",
|
||||||
black: app.black || false,
|
black: app.black || false,
|
||||||
|
translatable: app.translatable || false,
|
||||||
});
|
});
|
||||||
currentOpenAppId.value++;
|
currentOpenAppId.value++;
|
||||||
// Add to navbar
|
// Add to navbar
|
||||||
@ -397,6 +421,7 @@ const toggleMinWindow = (windowUUId: string) => {
|
|||||||
width: activeWindow.width,
|
width: activeWindow.width,
|
||||||
height: activeWindow.height,
|
height: activeWindow.height,
|
||||||
black: activeWindow.black || false,
|
black: activeWindow.black || false,
|
||||||
|
translatable: activeWindow.translatable || false,
|
||||||
lastpositionw: "",
|
lastpositionw: "",
|
||||||
lastpositionh: "",
|
lastpositionh: "",
|
||||||
});
|
});
|
||||||
@ -487,6 +512,32 @@ const openNewsSourcePage = async (slug: string, title: string) => {
|
|||||||
passedValues.value = null;
|
passedValues.value = null;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
const toggleTranslate = (id: string) => {
|
||||||
|
console.log("windowId", id);
|
||||||
|
applyForTranslation.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const translateAvailable = () => {};
|
||||||
|
|
||||||
|
// Load user config via HTTP requests to the server.
|
||||||
|
onMounted(async () => {
|
||||||
|
const loadUserInfoData = await loadUserInfo();
|
||||||
|
if (!loadUserInfoData.user) {
|
||||||
|
notLoggedInState.value = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
loadUserInfoData.langPref !== locale &&
|
||||||
|
langPrefDifferent.doNotShowLangPrefPopUp === false
|
||||||
|
) {
|
||||||
|
langPrefDifferent.value = true;
|
||||||
|
}
|
||||||
|
if (locale === "en" && loadUserInfoData.translate.enabled === true) {
|
||||||
|
applyForTranslation.value = true;
|
||||||
|
}
|
||||||
|
// Use Google as the default translate provider
|
||||||
|
translateProvider.value = loadUserInfoData.translate.provider || "google";
|
||||||
|
console.log(langPrefDifferent);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="changeLangAnimation">
|
<div v-if="changeLangAnimation">
|
||||||
@ -586,6 +637,33 @@ const openNewsSourcePage = async (slug: string, title: string) => {
|
|||||||
class="flex flex-col justify-center align-center text-center absolute w-full h-screen inset-x-0 inset-y-0 z-[-10]"
|
class="flex flex-col justify-center align-center text-center absolute w-full h-screen inset-x-0 inset-y-0 z-[-10]"
|
||||||
id="desktop"
|
id="desktop"
|
||||||
></div>
|
></div>
|
||||||
|
<!--Detect langPref different popup-->
|
||||||
|
<Dialog v-model:open="langPrefDifferent">
|
||||||
|
<DialogContent class="!border-0 !bg-black !rounded">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ t("settings.logout") }}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{{ t("popuptext.logout") }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
langPrefDifferent.value = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{{ t("popup.stay") }}
|
||||||
|
</Button>
|
||||||
|
<Button @click="() => switchLocalePath()" variant="outline">
|
||||||
|
{{ t("popup.change") }}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<!--Window system-->
|
||||||
<Transition>
|
<Transition>
|
||||||
<div>
|
<div>
|
||||||
<DraggableWindow
|
<DraggableWindow
|
||||||
@ -598,6 +676,9 @@ const openNewsSourcePage = async (slug: string, title: string) => {
|
|||||||
:height="window.height"
|
:height="window.height"
|
||||||
@click="obtainTopWindowPosition(window.id)"
|
@click="obtainTopWindowPosition(window.id)"
|
||||||
:black="window.black"
|
:black="window.black"
|
||||||
|
@translate="() => toggleTranslate(window.id)"
|
||||||
|
:notLoggedInState="notLoggedInState"
|
||||||
|
:windowTranslateState="window.translatable"
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Component
|
<Component
|
||||||
@ -610,6 +691,9 @@ const openNewsSourcePage = async (slug: string, title: string) => {
|
|||||||
:values="passedValues"
|
:values="passedValues"
|
||||||
:windows="activeWindows"
|
:windows="activeWindows"
|
||||||
@closeWindow="closeWindow"
|
@closeWindow="closeWindow"
|
||||||
|
:applyForTranslation="applyForTranslation"
|
||||||
|
:windowTranslateState="window.translatable"
|
||||||
|
:notLoggedInState="notLoggedInState"
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</DraggableWindow>
|
</DraggableWindow>
|
||||||
|
@ -17,5 +17,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
error: "ERR_NOT_USER_LOGIN",
|
error: "ERR_NOT_USER_LOGIN",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const checkUser = await sql``;
|
const verifyUserToken = await sql`
|
||||||
|
SELECT * FROM usertokens
|
||||||
|
where token=${readUserToken}
|
||||||
|
`;
|
||||||
|
if (verifyUserToken.length === 0) {
|
||||||
|
return {
|
||||||
|
error: "ERR_NOT_USER_LOGIN",
|
||||||
|
requested_action: "LOGOUT_USER",
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const buildURL = protocol + "://" + host + "/api/news/get/lt/" + slug;
|
const buildURL = protocol + "://" + host + "/api/news/get/lt/" + slug;
|
||||||
const data = await fetch(buildURL);
|
const data = await fetch(buildURL);
|
||||||
const fetchNewsArticle = await data.json();
|
const fetchNewsArticle = await data.json();
|
||||||
|
console.log(locale);
|
||||||
const chatCompletion = await groq.chat.completions.create({
|
const chatCompletion = await groq.chat.completions.create({
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const slug = getRouterParam(event, "slug");
|
|
||||||
const body = await readBody(event);
|
|
||||||
return {
|
|
||||||
body: body,
|
|
||||||
title: "News Org 1",
|
|
||||||
slug: "taisounds",
|
|
||||||
website: "https://yuanhau.com",
|
|
||||||
description: "wah wah wah wah wah wah I dont fucking care",
|
|
||||||
facebook: "https://www.facebook.csdkc",
|
|
||||||
logoUrl:
|
|
||||||
"https://cdn.discordapp.com/avatars/918723093646684180/4eecc27ac05ee8a701fa167808610c7a.jpg",
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,27 +0,0 @@
|
|||||||
import sql from "~/server/components/postgres";
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const body = await readBody(event);
|
|
||||||
const query = getQuery(event);
|
|
||||||
/*const sources = await sql`SELECT * FROM sources`;
|
|
||||||
return sources;*/
|
|
||||||
// Fake data
|
|
||||||
return {
|
|
||||||
status: "ok",
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "Source 1",
|
|
||||||
logo: "#",
|
|
||||||
url: "https://source1.com",
|
|
||||||
description: "Description for Source 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Source 2",
|
|
||||||
logo: "#",
|
|
||||||
url: "https://source2.com",
|
|
||||||
description: "Description for Source 2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
41
server/api/create_database.ts
Normal file
41
server/api/create_database.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import sql from "~/server/components/postgres";
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const createUsers = await sql`
|
||||||
|
create table if not exists users (
|
||||||
|
uuid text primary key,
|
||||||
|
created_at timestamptz default current_timestamp,
|
||||||
|
username text not null unique,
|
||||||
|
avatarurl text,
|
||||||
|
firstname text,
|
||||||
|
passwordhash text not null,
|
||||||
|
email text
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const usersList = await sql`
|
||||||
|
create table if not exists usertokens (
|
||||||
|
token text not null primary key,
|
||||||
|
created_at timestamptz default current_timestamp,
|
||||||
|
username text not null,
|
||||||
|
email text,
|
||||||
|
avatarurl text,
|
||||||
|
firstname text
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const createUserAiChatHistory = await sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_history (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
uuid VARCHAR(255) NOT NULL,
|
||||||
|
role VARCHAR(50) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`;
|
||||||
|
const createSources = await sql``;
|
||||||
|
return {
|
||||||
|
createUsers: createUsers,
|
||||||
|
usersList: usersList,
|
||||||
|
createUserAiChatHistory: createUserAiChatHistory,
|
||||||
|
createSources: createSources,
|
||||||
|
};
|
||||||
|
});
|
@ -37,6 +37,9 @@ function cleanUpSlug(orgslug: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
const translateQuery = getQuery(event).translate;
|
||||||
|
const translate = translateQuery === "true" ? true : false;
|
||||||
|
console.log(translate);
|
||||||
const slug = getRouterParam(event, "slug");
|
const slug = getRouterParam(event, "slug");
|
||||||
const cleanSlug = cleanUpSlug(slug);
|
const cleanSlug = cleanUpSlug(slug);
|
||||||
if (
|
if (
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import s3 from "~/server/components/s3";
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const slug = getRouterParam(event, "slug");
|
|
||||||
return sendRedirect(
|
|
||||||
event,
|
|
||||||
`${process.env.S3_ENDPOINT}/${process.env.S3_BUCKETNAME}/${slug}`,
|
|
||||||
302,
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,8 +1,50 @@
|
|||||||
// TODO Add caching
|
import sql from "~/server/components/postgres";
|
||||||
|
import CheckKidUnfriendlyContent from "~/components/checks/checkKidUnfriendlyContent";
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
// Caching
|
||||||
|
|
||||||
|
interface CacheItems {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
articles: any[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
const CACHE_DURATION = 1000 * 60 * 30;
|
||||||
|
const cache: Record<string, CacheItems> = {};
|
||||||
|
|
||||||
|
function cleanupCache() {
|
||||||
|
const now = Date.now();
|
||||||
|
Object.keys(cache).forEach((key) => {
|
||||||
|
if (now - cache[key].timestamp > CACHE_DURATION) {
|
||||||
|
delete cache[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function checks(title: string) {
|
||||||
|
const wordss = await pullWord();
|
||||||
|
const result = await CheckKidUnfriendlyContent(title, wordss);
|
||||||
|
checkResults.value.set(title, result);
|
||||||
|
console.log(title);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(cleanupCache, CACHE_DURATION);
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const slug = getRouterParam(event, "slug");
|
const slug = getRouterParam(event, "slug");
|
||||||
|
if (!slug) {
|
||||||
|
return {
|
||||||
|
error: "NO_SLUG_PROVIDED",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (cache[slug] && Date.now() - cache[slug].timestamp < CACHE_DURATION) {
|
||||||
|
return {
|
||||||
|
...cache[slug],
|
||||||
|
cached: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
const buildUrl = "https://today.line.me/tw/v3/publisher/" + slug;
|
const buildUrl = "https://today.line.me/tw/v3/publisher/" + slug;
|
||||||
try {
|
try {
|
||||||
const req = await fetch(buildUrl, {
|
const req = await fetch(buildUrl, {
|
||||||
@ -25,37 +67,45 @@ export default defineEventHandler(async (event) => {
|
|||||||
.text()
|
.text()
|
||||||
.replace(/.css-.*\}/, "");
|
.replace(/.css-.*\}/, "");
|
||||||
const description = html("p.description").text();
|
const description = html("p.description").text();
|
||||||
const logoClue = html("div.editor").contents();
|
|
||||||
const logo =
|
|
||||||
logoClue.find("img").attr("srcset") ||
|
|
||||||
html("div.editor div figure img").attr("src") ||
|
|
||||||
"";
|
|
||||||
const bgImage = html("figure.keyVisual img").attr("srcset") || "";
|
|
||||||
const articles = [];
|
|
||||||
const regexArticleLinks = /[a-zA-Z0-9]{7}/g;
|
const regexArticleLinks = /[a-zA-Z0-9]{7}/g;
|
||||||
const otherArticles = <any[]>[];
|
const otherArticles = <any[]>[];
|
||||||
html("a.ltcp-link").each((i, element) => {
|
html("a.ltcp-link").each((i, element) => {
|
||||||
const articleLink = html(element).attr("href");
|
const articleLink = html(element).attr("href");
|
||||||
const articleTitle = html(element).find("h3.header").text();
|
const articleTitle = html(element).find("h3.header").text();
|
||||||
|
//const image = html(element).find("figure").attr("src");
|
||||||
|
console.log(html(element).find("img"));
|
||||||
|
console.log("----------");
|
||||||
const date = html(element)
|
const date = html(element)
|
||||||
.find("div._articleCard div.css-wqleh6 span")
|
.find("div._articleCard div.css-wqleh6 span")
|
||||||
.text();
|
.text();
|
||||||
if (articleLink && articleTitle) {
|
if (articleLink && articleTitle) {
|
||||||
const articleSlug = articleLink.matchAll(regexArticleLinks);
|
const articleSlug = articleLink
|
||||||
|
.replaceAll("article", "")
|
||||||
|
.match(regexArticleLinks);
|
||||||
otherArticles.push({
|
otherArticles.push({
|
||||||
index: i,
|
index: i,
|
||||||
title: articleTitle,
|
title: articleTitle,
|
||||||
link: articleSlug,
|
link: articleSlug[0],
|
||||||
date: date,
|
date: date,
|
||||||
|
//image: image || "/geterrorassets/noImageLogo.svg",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
/*const pushNewsOrg = await sql`
|
||||||
|
insert into
|
||||||
|
`*/
|
||||||
|
cache[slug] = {
|
||||||
|
slug: slug,
|
||||||
|
title: newsOrgName,
|
||||||
|
description: description,
|
||||||
|
articles: otherArticles,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
title: newsOrgName,
|
title: newsOrgName,
|
||||||
description: description,
|
description: description,
|
||||||
logo: logo,
|
|
||||||
articles: otherArticles,
|
articles: otherArticles,
|
||||||
logoClue: String(logoClue),
|
cached: false,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
17
server/api/publishers/lt_all.ts
Normal file
17
server/api/publishers/lt_all.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import sql from "~/server/components/postgres";
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const fetchDataInSQL = await sql`
|
||||||
|
SELECT * FROM lt_news_org;
|
||||||
|
`;
|
||||||
|
return {
|
||||||
|
data: fetchDataInSQL,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
error: "SERVER_SIDE_ERR",
|
||||||
|
elogs: e.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
@ -1,3 +0,0 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
|
||||||
return {};
|
|
||||||
});
|
|
15
server/api/user/loadInfo.ts
Normal file
15
server/api/user/loadInfo.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Fixed data for testing
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
return {
|
||||||
|
langPref: "en",
|
||||||
|
doNotShowLangPrefPopUp: false,
|
||||||
|
email: "test@yuanhau.com",
|
||||||
|
name: "Howard",
|
||||||
|
useCustomGroqKey: true,
|
||||||
|
translate: {
|
||||||
|
enabled: true,
|
||||||
|
lang: "en",
|
||||||
|
provider: "google", // Default provider
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
@ -35,8 +35,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (fetchUserInfo.length === 0) {
|
if (fetchUserInfo.length === 0) {
|
||||||
const hashedPassword = await argon2.hash(salt + password);
|
const hashedPassword = await argon2.hash(salt + password);
|
||||||
const createNewUser = await sql`
|
const createNewUser = await sql`
|
||||||
insert into users (uuid, username, passwordhash)
|
insert into users (uuid, username, passwordhash, avatarurl)
|
||||||
values (${uuidv4()}, ${username}, ${hashedPassword})
|
values (${uuidv4()}, ${username}, ${hashedPassword}, ${defaultAvatarUrl})
|
||||||
`;
|
`;
|
||||||
console.log(createNewUser);
|
console.log(createNewUser);
|
||||||
if (fetchUserInfo.length !== 0) {
|
if (fetchUserInfo.length !== 0) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user