Compare commits

...

19 Commits

Author SHA1 Message Date
b356afe766 Push translate system? 2025-06-07 08:03:44 +08:00
028b545374 Change readme links & update news to NOT use the translated Hi as a test
& updated the draggable window to remove the testing state. (I know
testing in prod is bad.)
2025-06-07 01:25:45 +08:00
3abfe46464 EXTREMELY basic translating in the newsView page. & made the about
window i18n available & added GOALS_BEFORE_NEXT_DEVLOG.md file to track
what goals should I hit before getting a another devlog.
2025-06-07 01:19:48 +08:00
d2099074a7 Add ZH_TW README & translations. 2025-06-07 00:42:57 +08:00
61a7ecbf12 Basic translations? 2025-06-07 00:04:11 +08:00
7314a5fa8a Remove exploitable plugin
(google-translate-api) use translate instead
2025-06-06 15:08:27 +08:00
a4a3822a49 Last commit forgot : 2025-06-06 15:00:15 +08:00
bae0d3b8dc Add traslation plugins & add loadUserInfo to get pref lang & the
translate provider & which windows can use the translate feat or not.
Not sure how can I intergrate it tho 🤔
2025-06-06 15:00:05 +08:00
9bf177f971 Add apis into the api page & for now (devlog demo) don't save stuff into
the db.
2025-06-05 23:57:55 +08:00
64f4babe95 Fix & add profile url thingy 2025-06-05 23:51:33 +08:00
083fae51de Made the about news org clickable & commit somethings that are not
pushed in the last commit.
2025-06-05 15:48:52 +08:00
4d49554a0e Add .dev.env & update basic logoutuser.ts & made a basic blur login
system & also normal logging in broke  :(
2025-06-05 12:03:05 +08:00
b8438f7f33 Update docs. 2025-06-05 00:04:14 +08:00
48f897ed63 Fix x2? 2025-06-04 22:55:09 +08:00
9b9ebb7f50 Update README 2025-06-04 22:51:08 +08:00
feb679c3ef Fix 2025-06-04 22:38:17 +08:00
8032c3faae Delete old components from a month ago? & Update draggable window to be using the native svgs by lucide icons & updated the news page to activate the tab changing animation when changing tabs & added caching into the [slug].ts file in publishers/lt & added a basic endpoint for searching for sources. 2025-06-04 22:30:02 +08:00
231a7ce251 Add pending animation (with the help of GitHub Copilot) 2025-06-04 20:46:00 +08:00
0298a5ae90 Styles for the other articles. 2025-06-04 10:19:20 +08:00
36 changed files with 975 additions and 253 deletions

29
.dev.env Normal file
View 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

View File

@ -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

View 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
View File

@ -0,0 +1,235 @@
# 新聞解析 / News Analyze
[English Version](/README.md) [繁體中文版](/README.ZH_TW.md)
![](https://hackatime-badge.hackclub.com/U087ATD163V/news-analyize) ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/hpware/news-analyze?utm_source=oss&utm_medium=github&utm_campaign=hpware%2Fnews-analyze&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) ![LICENSE](https://img.shields.io/github/license/hpware/news-analyze?style=flat) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/hpware/news-analyze/build_docker_image.yml)
一個 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
## 預覽系統:
### 首頁:
![](/.github/README/home.png)
### 桌面程式:
![](/.github/README/desktop.png)
## 如何在我的電腦上運行?
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 :))

View File

@ -1,14 +1,19 @@
# 新聞解析 / News Analyze # 新聞解析 / News Analyze
[English Version](/README.md)&nbsp;[繁體中文版](/README.ZH_TW.md)
![](https://hackatime-badge.hackclub.com/U087ATD163V/news-analyize)&nbsp;![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/hpware/news-analyze?utm_source=oss&utm_medium=github&utm_campaign=hpware%2Fnews-analyze&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)&nbsp;![LICENSE](https://img.shields.io/github/license/hpware/news-analyze?style=flat)&nbsp;![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/hpware/news-analyze/build_docker_image.yml) ![](https://hackatime-badge.hackclub.com/U087ATD163V/news-analyize)&nbsp;![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/hpware/news-analyze?utm_source=oss&utm_medium=github&utm_campaign=hpware%2Fnews-analyze&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)&nbsp;![LICENSE](https://img.shields.io/github/license/hpware/news-analyze?style=flat)&nbsp;![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/hpware/news-analyze/build_docker_image.yml)
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:
![](/.github/README/desktop.png) ![](/.github/README/desktop.png)
## 如何執行 ## 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

View File

@ -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=="],

View File

@ -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>

View File

@ -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>

View File

@ -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"
> >
<img <div class="flex flex-col gap-3 text-left w-full">
:src="fetchNewsOrgInfo?.logoUrl" <h1
class="w-48 h-48 rounded-l-3xl object-cover p-0 m-0" v-if="pending"
draggable="false" class="h-12 bg-gray-200 animate-pulse rounded m-2 w-3/4 mx-auto"
/> ></h1>
<div class="flex flex-col gap-3 text-left"> <h1
<h1 class="text-4xl font-bold m-3 text-left" ref="orgNameAnimation"> v-else
class="text-4xl font-bold m-2 text-center"
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 <div class="h-4 bg-gray-200 animate-pulse rounded w-5/6"></div>
class="gap-[3px] flex flex-row text-center align-center justify-center" <div class="h-4 bg-gray-200 animate-pulse rounded w-4/6"></div>
>
<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> <span v-else class="text-ms m-1 mt-5 text-left text-wrap">
</div>--> {{ fetchNewsOrgInfo?.description }}
<div class="flex flex-col gap-3 text-left"> </span>
<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>
<div v-for="item in fetchNewsOrgInfo?.articles"> <div class="space-y-3">
{{ item.title }} <template v-if="pending">
</div> <div
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"
>
<div>
<div class="flex flex-col">
<span class="title text-bold texxt-sm">{{
item.title.replaceAll("獨家專欄》", "")
}}</span>
<span class="date text-xs">{{ item.date }}</span>
</div>
</div>
</button>
</template>
</div> </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>

View File

@ -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>

View File

@ -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">
<button <!-- Tabs Loading State -->
v-for="item in tabs" <template v-if="canNotLoadTabUI">
@click="updateContent(item.url, true)" <div
:class=" v-for="n in 5"
isPrimary(item.url, true) ? 'text-sky-600 text-bold' : 'text-black' :key="n"
" class="h-8 w-20 bg-gray-400/50 animate-pulse rounded mx-1"
class="disabled:cursor-not-allowed" ></div>
:disabled="isPrimary(item.url, true) || switchTabs" </template>
>
<span>{{ item.text }}</span> <!-- Actual Tabs -->
</button> <template v-else>
<button
v-for="item in tabs"
@click="updateContent(item.url, true)"
:class="
isPrimary(item.url, true)
? 'text-sky-600 text-bold'
: 'text-black'
"
class="disabled:cursor-not-allowed"
:disabled="isPrimary(item.url, true) || switchTabs"
>
<span>{{ true ? item.text : testmessage }}</span>
</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">
</div> <!-- Title Skeleton -->
</Transition> <div
<Transition class="h-8 bg-gray-300 animate-pulse rounded-lg w-3/4 mx-auto mb-2"
enter-active-class="animate__animated animate__fadeIn" ></div>
leave-active-class="animate__animated animate__fadeOut"
> <!-- Publisher and Date Skeleton -->
<div v-if="!switchTabs"> <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>
<!-- Action Button Skeleton -->
<div class="flex justify-center mb-2">
<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
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> </template>
</Transition> </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>

View File

@ -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">

View File

@ -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",

View File

@ -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>

View 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
View File

@ -0,0 +1,3 @@
export default function logoutuser() {
return;
}

View File

@ -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
View 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
```

View File

@ -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": "developeryh",
"contactEmailStarter": "Contact Email"
},
"copyrightInfo": "Copyright Info"
} }
} }

View File

@ -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": "版權資訊"
} }
} }

View File

@ -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"

View File

@ -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]",

View File

@ -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>

View File

@ -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",
};
}
}); });

View File

@ -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: [
{ {

View File

@ -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",
};
});

View File

@ -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",
},
],
};
});

View 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,
};
});

View File

@ -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 (

View File

@ -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,
);
});

View File

@ -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);

View 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,
};
}
});

View File

@ -1,3 +0,0 @@
export default defineEventHandler(async (event) => {
return {};
});

View 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
},
};
});

View File

@ -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) {