Compare commits
2 Commits
d4ad3ed990
...
057087a6be
Author | SHA1 | Date | |
---|---|---|---|
057087a6be | |||
3cb69f6e7c |
@ -5,7 +5,7 @@ on:
|
|||||||
- published
|
- published
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
frontend-deployment:
|
deployment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -11,6 +11,8 @@ As for the more detailed development approach, I plan to use Clean Architecture
|
|||||||
|
|
||||||
These will allow me to become more proficient in these modern development practices and leave a lot of flexibility and room for adjustments in the future.
|
These will allow me to become more proficient in these modern development practices and leave a lot of flexibility and room for adjustments in the future.
|
||||||
|
|
||||||
|
For more information about the development process, you can check out the [project board](https://git.squidspirit.com/squid/blog/projects). As for the details of the architecture and convention, you can find them in the [wiki](https://git.squidspirit.com/squid/blog/wiki).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project uses a combination of the [MIT License and a custom license](./LICENSE.md). Based on the MIT License, anyone is permitted to use the code. However, before deploying the code, they must first replace any information belonging to "me" or any content that could identify "me," such as logos, names, and "about me" sections.
|
This project uses a combination of the [MIT License and a custom license](./LICENSE.md). Based on the MIT License, anyone is permitted to use the code. However, before deploying the code, they must first replace any information belonging to "me" or any content that could identify "me," such as logos, names, and "about me" sections.
|
||||||
|
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@ -1789,6 +1789,7 @@ dependencies = [
|
|||||||
"actix-web",
|
"actix-web",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"percent-encoding",
|
||||||
"post",
|
"post",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
]
|
]
|
||||||
|
@ -14,6 +14,7 @@ dotenv = "0.15.0"
|
|||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
|
percent-encoding = "2.3.1"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
sqlx = { version = "0.8.5", features = [
|
sqlx = { version = "0.8.5", features = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -68,21 +68,23 @@ impl PostDbService for PostDbServiceImpl {
|
|||||||
|
|
||||||
let mut post_info_mappers_map = HashMap::<i32, PostInfoMapper>::new();
|
let mut post_info_mappers_map = HashMap::<i32, PostInfoMapper>::new();
|
||||||
|
|
||||||
for record in records {
|
for record in &records {
|
||||||
let post_info = post_info_mappers_map
|
let post_info = post_info_mappers_map
|
||||||
.entry(record.post_id)
|
.entry(record.post_id)
|
||||||
.or_insert_with(|| PostInfoMapper {
|
.or_insert_with(|| PostInfoMapper {
|
||||||
id: record.post_id,
|
id: record.post_id,
|
||||||
title: record.title,
|
title: record.title.clone(),
|
||||||
description: record.description,
|
description: record.description.clone(),
|
||||||
preview_image_url: record.preview_image_url,
|
preview_image_url: record.preview_image_url.clone(),
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
published_time: record.published_time,
|
published_time: record.published_time,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let (Some(label_id), Some(label_name), Some(label_color)) =
|
if let (Some(label_id), Some(label_name), Some(label_color)) = (
|
||||||
(record.label_id, record.label_name, record.label_color)
|
record.label_id,
|
||||||
{
|
record.label_name.clone(),
|
||||||
|
record.label_color,
|
||||||
|
) {
|
||||||
post_info.labels.push(LabelMapper {
|
post_info.labels.push(LabelMapper {
|
||||||
id: label_id,
|
id: label_id,
|
||||||
name: label_name,
|
name: label_name,
|
||||||
@ -93,7 +95,14 @@ impl PostDbService for PostDbServiceImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(post_info_mappers_map.into_values().collect())
|
let mut ordered_posts = Vec::new();
|
||||||
|
for record in &records {
|
||||||
|
if let Some(post_info) = post_info_mappers_map.remove(&record.post_id) {
|
||||||
|
ordered_posts.push(post_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ordered_posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_full_post(&self, id: i32) -> Result<PostMapper, PostError> {
|
async fn get_full_post(&self, id: i32) -> Result<PostMapper, PostError> {
|
||||||
@ -135,25 +144,27 @@ impl PostDbService for PostDbServiceImpl {
|
|||||||
|
|
||||||
let mut post_mappers_map = HashMap::<i32, PostMapper>::new();
|
let mut post_mappers_map = HashMap::<i32, PostMapper>::new();
|
||||||
|
|
||||||
for record in records {
|
for record in &records {
|
||||||
let post = post_mappers_map
|
let post = post_mappers_map
|
||||||
.entry(record.post_id)
|
.entry(record.post_id)
|
||||||
.or_insert_with(|| PostMapper {
|
.or_insert_with(|| PostMapper {
|
||||||
id: record.post_id,
|
id: record.post_id,
|
||||||
info: PostInfoMapper {
|
info: PostInfoMapper {
|
||||||
id: record.post_id,
|
id: record.post_id,
|
||||||
title: record.title,
|
title: record.title.clone(),
|
||||||
description: record.description,
|
description: record.description.clone(),
|
||||||
preview_image_url: record.preview_image_url,
|
preview_image_url: record.preview_image_url.clone(),
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
published_time: record.published_time,
|
published_time: record.published_time,
|
||||||
},
|
},
|
||||||
content: record.content,
|
content: record.content.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if let (Some(label_id), Some(label_name), Some(label_color)) =
|
if let (Some(label_id), Some(label_name), Some(label_color)) = (
|
||||||
(record.label_id, record.label_name, record.label_color)
|
record.label_id,
|
||||||
{
|
record.label_name.clone(),
|
||||||
|
record.label_color,
|
||||||
|
) {
|
||||||
post.info.labels.push(LabelMapper {
|
post.info.labels.push(LabelMapper {
|
||||||
id: label_id,
|
id: label_id,
|
||||||
name: label_name,
|
name: label_name,
|
||||||
|
@ -7,6 +7,7 @@ edition.workspace = true
|
|||||||
actix-web.workspace = true
|
actix-web.workspace = true
|
||||||
dotenv.workspace = true
|
dotenv.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
percent-encoding.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
|
||||||
post.workspace = true
|
post.workspace = true
|
||||||
|
@ -34,9 +34,13 @@ async fn init_database() -> Pool<Postgres> {
|
|||||||
let user = env::var("DATABASE_USER").unwrap_or_else(|_| "postgres".to_string());
|
let user = env::var("DATABASE_USER").unwrap_or_else(|_| "postgres".to_string());
|
||||||
let password = env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "".to_string());
|
let password = env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "".to_string());
|
||||||
let dbname = env::var("DATABASE_NAME").unwrap_or_else(|_| "postgres".to_string());
|
let dbname = env::var("DATABASE_NAME").unwrap_or_else(|_| "postgres".to_string());
|
||||||
|
|
||||||
|
let encoded_password =
|
||||||
|
percent_encoding::utf8_percent_encode(&password, percent_encoding::NON_ALPHANUMERIC)
|
||||||
|
.to_string();
|
||||||
let database_url = format!(
|
let database_url = format!(
|
||||||
"postgres://{}:{}@{}:{}/{}",
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
user, password, host, port, dbname
|
user, encoded_password, host, port, dbname
|
||||||
);
|
);
|
||||||
|
|
||||||
let db_pool = PgPoolOptions::new()
|
let db_pool = PgPoolOptions::new()
|
||||||
|
@ -23,13 +23,17 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"sanitize-html": "^2.17.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
143
frontend/pnpm-lock.yaml
generated
143
frontend/pnpm-lock.yaml
generated
@ -35,6 +35,12 @@ importers:
|
|||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.1.11(vite@7.0.5(jiti@2.4.2)(lightningcss@1.30.1))
|
version: 4.1.11(vite@7.0.5(jiti@2.4.2)(lightningcss@1.30.1))
|
||||||
|
'@types/markdown-it':
|
||||||
|
specifier: ^14.1.2
|
||||||
|
version: 14.1.2
|
||||||
|
'@types/sanitize-html':
|
||||||
|
specifier: ^2.16.0
|
||||||
|
version: 2.16.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.18.0
|
specifier: ^9.18.0
|
||||||
version: 9.31.0(jiti@2.4.2)
|
version: 9.31.0(jiti@2.4.2)
|
||||||
@ -47,6 +53,9 @@ importers:
|
|||||||
globals:
|
globals:
|
||||||
specifier: ^16.0.0
|
specifier: ^16.0.0
|
||||||
version: 16.3.0
|
version: 16.3.0
|
||||||
|
markdown-it:
|
||||||
|
specifier: ^14.1.0
|
||||||
|
version: 14.1.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.4.2
|
specifier: ^3.4.2
|
||||||
version: 3.6.2
|
version: 3.6.2
|
||||||
@ -56,6 +65,9 @@ importers:
|
|||||||
prettier-plugin-tailwindcss:
|
prettier-plugin-tailwindcss:
|
||||||
specifier: ^0.6.11
|
specifier: ^0.6.11
|
||||||
version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.36.13))(prettier@3.6.2)
|
version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.36.13))(prettier@3.6.2)
|
||||||
|
sanitize-html:
|
||||||
|
specifier: ^2.17.0
|
||||||
|
version: 2.17.0
|
||||||
svelte:
|
svelte:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.36.13
|
version: 5.36.13
|
||||||
@ -622,9 +634,21 @@ packages:
|
|||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/linkify-it@5.0.0':
|
||||||
|
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||||
|
|
||||||
|
'@types/markdown-it@14.1.2':
|
||||||
|
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||||
|
|
||||||
|
'@types/mdurl@2.0.0':
|
||||||
|
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||||
|
|
||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
|
'@types/sanitize-html@2.16.0':
|
||||||
|
resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.38.0':
|
'@typescript-eslint/eslint-plugin@8.38.0':
|
||||||
resolution: {integrity: sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==}
|
resolution: {integrity: sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -794,10 +818,27 @@ packages:
|
|||||||
devalue@5.1.1:
|
devalue@5.1.1:
|
||||||
resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==}
|
resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==}
|
||||||
|
|
||||||
|
dom-serializer@2.0.0:
|
||||||
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
|
||||||
|
domelementtype@2.3.0:
|
||||||
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
|
|
||||||
|
domhandler@5.0.3:
|
||||||
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
domutils@3.2.2:
|
||||||
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
|
|
||||||
enhanced-resolve@5.18.2:
|
enhanced-resolve@5.18.2:
|
||||||
resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
|
resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
esbuild@0.25.8:
|
esbuild@0.25.8:
|
||||||
resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
|
resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -955,6 +996,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
htmlparser2@8.0.2:
|
||||||
|
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@ -990,6 +1034,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
|
is-plain-object@5.0.0:
|
||||||
|
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
is-reference@1.2.1:
|
is-reference@1.2.1:
|
||||||
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
|
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
|
||||||
|
|
||||||
@ -1098,6 +1146,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
linkify-it@5.0.0:
|
||||||
|
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||||
|
|
||||||
locate-character@3.0.0:
|
locate-character@3.0.0:
|
||||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||||
|
|
||||||
@ -1117,6 +1168,13 @@ packages:
|
|||||||
magic-string@0.30.17:
|
magic-string@0.30.17:
|
||||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||||
|
|
||||||
|
markdown-it@14.1.0:
|
||||||
|
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
mdurl@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||||
|
|
||||||
merge2@1.4.1:
|
merge2@1.4.1:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -1180,6 +1238,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
parse-srcset@1.0.2:
|
||||||
|
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
|
||||||
|
|
||||||
path-exists@4.0.0:
|
path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1314,6 +1375,10 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
punycode.js@2.3.1:
|
||||||
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1350,6 +1415,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
sanitize-html@2.17.0:
|
||||||
|
resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==}
|
||||||
|
|
||||||
semver@7.7.2:
|
semver@7.7.2:
|
||||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1452,6 +1520,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uc.micro@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||||
|
|
||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
@ -1950,8 +2021,21 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/linkify-it@5.0.0': {}
|
||||||
|
|
||||||
|
'@types/markdown-it@14.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@types/linkify-it': 5.0.0
|
||||||
|
'@types/mdurl': 2.0.0
|
||||||
|
|
||||||
|
'@types/mdurl@2.0.0': {}
|
||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
|
'@types/sanitize-html@2.16.0':
|
||||||
|
dependencies:
|
||||||
|
htmlparser2: 8.0.2
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.1
|
'@eslint-community/regexpp': 4.12.1
|
||||||
@ -2130,11 +2214,31 @@ snapshots:
|
|||||||
|
|
||||||
devalue@5.1.1: {}
|
devalue@5.1.1: {}
|
||||||
|
|
||||||
|
dom-serializer@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
entities: 4.5.0
|
||||||
|
|
||||||
|
domelementtype@2.3.0: {}
|
||||||
|
|
||||||
|
domhandler@5.0.3:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
|
domutils@3.2.2:
|
||||||
|
dependencies:
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
|
||||||
enhanced-resolve@5.18.2:
|
enhanced-resolve@5.18.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.2.2
|
tapable: 2.2.2
|
||||||
|
|
||||||
|
entities@4.5.0: {}
|
||||||
|
|
||||||
esbuild@0.25.8:
|
esbuild@0.25.8:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.25.8
|
'@esbuild/aix-ppc64': 0.25.8
|
||||||
@ -2334,6 +2438,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
htmlparser2@8.0.2:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.2.2
|
||||||
|
entities: 4.5.0
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
@ -2359,6 +2470,8 @@ snapshots:
|
|||||||
|
|
||||||
is-number@7.0.0: {}
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
|
is-plain-object@5.0.0: {}
|
||||||
|
|
||||||
is-reference@1.2.1:
|
is-reference@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@ -2441,6 +2554,10 @@ snapshots:
|
|||||||
|
|
||||||
lilconfig@2.1.0: {}
|
lilconfig@2.1.0: {}
|
||||||
|
|
||||||
|
linkify-it@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
locate-character@3.0.0: {}
|
locate-character@3.0.0: {}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
@ -2457,6 +2574,17 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.4
|
'@jridgewell/sourcemap-codec': 1.5.4
|
||||||
|
|
||||||
|
markdown-it@14.1.0:
|
||||||
|
dependencies:
|
||||||
|
argparse: 2.0.1
|
||||||
|
entities: 4.5.0
|
||||||
|
linkify-it: 5.0.0
|
||||||
|
mdurl: 2.0.0
|
||||||
|
punycode.js: 2.3.1
|
||||||
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
|
mdurl@2.0.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
micromatch@4.0.8:
|
micromatch@4.0.8:
|
||||||
@ -2511,6 +2639,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
|
|
||||||
|
parse-srcset@1.0.2: {}
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
@ -2569,6 +2699,8 @@ snapshots:
|
|||||||
|
|
||||||
prettier@3.6.2: {}
|
prettier@3.6.2: {}
|
||||||
|
|
||||||
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
@ -2619,6 +2751,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mri: 1.2.0
|
mri: 1.2.0
|
||||||
|
|
||||||
|
sanitize-html@2.17.0:
|
||||||
|
dependencies:
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
escape-string-regexp: 4.0.0
|
||||||
|
htmlparser2: 8.0.2
|
||||||
|
is-plain-object: 5.0.0
|
||||||
|
parse-srcset: 1.0.2
|
||||||
|
postcss: 8.5.6
|
||||||
|
|
||||||
semver@7.7.2: {}
|
semver@7.7.2: {}
|
||||||
|
|
||||||
set-cookie-parser@2.7.1: {}
|
set-cookie-parser@2.7.1: {}
|
||||||
@ -2730,6 +2871,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.8.3: {}
|
typescript@5.8.3: {}
|
||||||
|
|
||||||
|
uc.micro@2.1.0: {}
|
||||||
|
|
||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'HackNerdMono';
|
font-family: 'HackNerdMono';
|
||||||
@ -25,14 +26,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-white font-sans text-base font-normal text-gray-800;
|
@apply bg-white font-sans text-base font-normal text-gray-700;
|
||||||
}
|
|
||||||
|
|
||||||
pre,
|
|
||||||
code,
|
|
||||||
kbd,
|
|
||||||
samp {
|
|
||||||
@apply font-mono;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
7
frontend/src/app.d.ts
vendored
7
frontend/src/app.d.ts
vendored
@ -3,7 +3,12 @@
|
|||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
|
||||||
|
interface Locals {
|
||||||
|
postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc;
|
||||||
|
postBloc: import('$lib/post/adapter/presenter/postBloc').PostBloc;
|
||||||
|
}
|
||||||
|
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
|
19
frontend/src/hooks.server.ts
Normal file
19
frontend/src/hooks.server.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl';
|
||||||
|
import { PostBloc } from '$lib/post/adapter/presenter/postBloc';
|
||||||
|
import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc';
|
||||||
|
import { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
||||||
|
import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
|
||||||
|
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
|
||||||
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const handle: Handle = ({ event, resolve }) => {
|
||||||
|
const postApiService = new PostApiServiceImpl(event.fetch);
|
||||||
|
const postRepository = new PostRepositoryImpl(postApiService);
|
||||||
|
const getAllPostsUseCase = new GetAllPostsUseCase(postRepository);
|
||||||
|
const getPostUseCase = new GetPostUseCase(postRepository);
|
||||||
|
|
||||||
|
event.locals.postListBloc = new PostListBloc(getAllPostsUseCase);
|
||||||
|
event.locals.postBloc = new PostBloc(getPostUseCase);
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
};
|
@ -5,12 +5,14 @@ export enum StatusType {
|
|||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IdleState {
|
export interface IdleState<T> {
|
||||||
status: StatusType.Idle;
|
status: StatusType.Idle;
|
||||||
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadingState {
|
export interface LoadingState<T> {
|
||||||
status: StatusType.Loading;
|
status: StatusType.Loading;
|
||||||
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SuccessState<T> {
|
export interface SuccessState<T> {
|
||||||
@ -18,9 +20,10 @@ export interface SuccessState<T> {
|
|||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorState {
|
export interface ErrorState<T> {
|
||||||
status: StatusType.Error;
|
status: StatusType.Error;
|
||||||
|
data?: T;
|
||||||
error: Error;
|
error: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncState<T> = IdleState | LoadingState | SuccessState<T> | ErrorState;
|
export type AsyncState<T> = IdleState<T> | LoadingState<T> | SuccessState<T> | ErrorState<T>;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="border-t border-gray-300">
|
<div class="border-t border-gray-300">
|
||||||
<div
|
<div
|
||||||
class="mx-auto flex max-w-screen-xl flex-col items-center justify-center gap-4 px-4 py-12 md:flex-row md:px-6"
|
class="mx-auto flex max-w-screen-xl flex-col items-center justify-center gap-4 px-4 py-12 text-gray-600 md:flex-row md:px-6"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row items-center justify-center gap-x-4">
|
<div class="flex flex-row items-center justify-center gap-x-4">
|
||||||
<a href="https://www.youtube.com/@squidspirit16" target="_blank" aria-label="YouTube Channel">
|
<a href="https://www.youtube.com/@squidspirit16" target="_blank" aria-label="YouTube Channel">
|
||||||
|
11
frontend/src/lib/common/framework/ui/SafeHtml.svelte
Normal file
11
frontend/src/lib/common/framework/ui/SafeHtml.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/* eslint-disable svelte/no-at-html-tags */
|
||||||
|
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
|
||||||
|
const { html }: { html: string } = $props();
|
||||||
|
|
||||||
|
const sanitizedHtml = $derived(sanitizeHtml(html));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@html sanitizedHtml}
|
@ -1,5 +1,7 @@
|
|||||||
import type { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto';
|
import type { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto';
|
||||||
|
import type { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto';
|
||||||
|
|
||||||
export interface PostApiService {
|
export interface PostApiService {
|
||||||
getAllPosts(): Promise<PostInfoResponseDto[]>;
|
getAllPosts(): Promise<PostInfoResponseDto[]>;
|
||||||
|
getPost(id: number): Promise<PostResponseDto | null>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
||||||
import type { PostRepository } from '$lib/post/application/repository/postRepository';
|
import type { PostRepository } from '$lib/post/application/repository/postRepository';
|
||||||
|
import type { Post } from '$lib/post/domain/entity/post';
|
||||||
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
|
|
||||||
export class PostRepositoryImpl implements PostRepository {
|
export class PostRepositoryImpl implements PostRepository {
|
||||||
@ -9,4 +10,9 @@ export class PostRepositoryImpl implements PostRepository {
|
|||||||
const dtos = await this.postApiService.getAllPosts();
|
const dtos = await this.postApiService.getAllPosts();
|
||||||
return dtos.map((dto) => dto.toEntity());
|
return dtos.map((dto) => dto.toEntity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPost(id: number): Promise<Post | null> {
|
||||||
|
const dto = await this.postApiService.getPost(id);
|
||||||
|
return dto?.toEntity() ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
41
frontend/src/lib/post/adapter/gateway/postResponseDto.ts
Normal file
41
frontend/src/lib/post/adapter/gateway/postResponseDto.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
PostInfoResponseDto,
|
||||||
|
PostInfoResponseSchema
|
||||||
|
} from '$lib/post/adapter/gateway/postInfoResponseDto';
|
||||||
|
import { Post } from '$lib/post/domain/entity/post';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
export const PostResponseSchema = z.object({
|
||||||
|
id: z.int32(),
|
||||||
|
info: PostInfoResponseSchema,
|
||||||
|
content: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export class PostResponseDto {
|
||||||
|
readonly id: number;
|
||||||
|
readonly info: PostInfoResponseDto;
|
||||||
|
readonly content: string;
|
||||||
|
|
||||||
|
private constructor(props: { id: number; info: PostInfoResponseDto; content: string }) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.info = props.info;
|
||||||
|
this.content = props.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: unknown): PostResponseDto {
|
||||||
|
const parsedJson = PostResponseSchema.parse(json);
|
||||||
|
return new PostResponseDto({
|
||||||
|
id: parsedJson.id,
|
||||||
|
info: PostInfoResponseDto.fromJson(parsedJson.info),
|
||||||
|
content: parsedJson.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toEntity(): Post {
|
||||||
|
return new Post({
|
||||||
|
id: this.id,
|
||||||
|
info: this.info.toEntity(),
|
||||||
|
content: this.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -56,6 +56,10 @@ export class ColorViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static rehydrate(props: DehydratedColorProps): ColorViewModel {
|
||||||
|
return new ColorViewModel(props);
|
||||||
|
}
|
||||||
|
|
||||||
get hex(): string {
|
get hex(): string {
|
||||||
const toHex = (value: number) => value.toString(16).padStart(2, '0');
|
const toHex = (value: number) => value.toString(16).padStart(2, '0');
|
||||||
return `#${toHex(this.red)}${toHex(this.green)}${toHex(this.blue)}${toHex(this.alpha)}`;
|
return `#${toHex(this.red)}${toHex(this.green)}${toHex(this.blue)}${toHex(this.alpha)}`;
|
||||||
@ -105,6 +109,15 @@ export class ColorViewModel {
|
|||||||
darken(amount: number): ColorViewModel {
|
darken(amount: number): ColorViewModel {
|
||||||
return this.lighten(-amount);
|
return this.lighten(-amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dehydrate(): DehydratedColorProps {
|
||||||
|
return {
|
||||||
|
red: this.red,
|
||||||
|
green: this.green,
|
||||||
|
blue: this.blue,
|
||||||
|
alpha: this.alpha
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Hsl {
|
interface Hsl {
|
||||||
@ -112,3 +125,10 @@ interface Hsl {
|
|||||||
s: number;
|
s: number;
|
||||||
l: number;
|
l: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DehydratedColorProps {
|
||||||
|
red: number;
|
||||||
|
green: number;
|
||||||
|
blue: number;
|
||||||
|
alpha: number;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { ColorViewModel } from '$lib/post/adapter/presenter/colorViewModel';
|
import {
|
||||||
|
ColorViewModel,
|
||||||
|
type DehydratedColorProps
|
||||||
|
} from '$lib/post/adapter/presenter/colorViewModel';
|
||||||
import type { Label } from '$lib/post/domain/entity/label';
|
import type { Label } from '$lib/post/domain/entity/label';
|
||||||
|
|
||||||
export class LabelViewModel {
|
export class LabelViewModel {
|
||||||
@ -19,4 +22,26 @@ export class LabelViewModel {
|
|||||||
color: ColorViewModel.fromEntity(label.color)
|
color: ColorViewModel.fromEntity(label.color)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static rehydrate(props: DehydratedLabelProps): LabelViewModel {
|
||||||
|
return new LabelViewModel({
|
||||||
|
id: props.id,
|
||||||
|
name: props.name,
|
||||||
|
color: ColorViewModel.rehydrate(props.color)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dehydrate(): DehydratedLabelProps {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
color: this.color.dehydrate()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DehydratedLabelProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
color: DehydratedColorProps;
|
||||||
}
|
}
|
||||||
|
62
frontend/src/lib/post/adapter/presenter/postBloc.ts
Normal file
62
frontend/src/lib/post/adapter/presenter/postBloc.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||||
|
import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
||||||
|
import type { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type PostState = AsyncState<PostViewModel>;
|
||||||
|
export type PostEvent = PostLoadedEvent;
|
||||||
|
|
||||||
|
export class PostBloc {
|
||||||
|
private readonly state = writable<PostState>({
|
||||||
|
status: StatusType.Idle
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly getPostUseCase: GetPostUseCase,
|
||||||
|
initialData?: PostViewModel
|
||||||
|
) {
|
||||||
|
this.state.set({
|
||||||
|
status: StatusType.Idle,
|
||||||
|
data: initialData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get subscribe() {
|
||||||
|
return this.state.subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
async dispatch(event: PostEvent): Promise<PostState> {
|
||||||
|
switch (event.event) {
|
||||||
|
case PostEventType.PostLoadedEvent:
|
||||||
|
return this.loadPost(event.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadPost(id: number): Promise<PostState> {
|
||||||
|
this.state.set({ status: StatusType.Loading, data: get(this.state).data });
|
||||||
|
|
||||||
|
const post = await this.getPostUseCase.execute(id);
|
||||||
|
if (!post) {
|
||||||
|
this.state.set({ status: StatusType.Error, error: new Error('Post not found') });
|
||||||
|
return get(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
const postViewModel = PostViewModel.fromEntity(post);
|
||||||
|
const result: PostState = {
|
||||||
|
status: StatusType.Success,
|
||||||
|
data: postViewModel
|
||||||
|
};
|
||||||
|
|
||||||
|
this.state.set(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PostEventType {
|
||||||
|
PostLoadedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostLoadedEvent {
|
||||||
|
event: PostEventType.PostLoadedEvent;
|
||||||
|
id: number;
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
import { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel';
|
import {
|
||||||
|
LabelViewModel,
|
||||||
|
type DehydratedLabelProps
|
||||||
|
} from '$lib/post/adapter/presenter/labelViewModel';
|
||||||
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
|
|
||||||
export class PostInfoViewModel {
|
export class PostInfoViewModel {
|
||||||
@ -35,4 +38,39 @@ export class PostInfoViewModel {
|
|||||||
publishedTime: postInfo.publishedTime
|
publishedTime: postInfo.publishedTime
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static rehydrate(props: DehydratedPostInfoProps): PostInfoViewModel {
|
||||||
|
return new PostInfoViewModel({
|
||||||
|
id: props.id,
|
||||||
|
title: props.title,
|
||||||
|
description: props.description,
|
||||||
|
previewImageUrl: new URL(props.previewImageUrl),
|
||||||
|
labels: props.labels.map((label) => LabelViewModel.rehydrate(label)),
|
||||||
|
publishedTime: new Date(props.publishedTime)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get formattedPublishedTime(): string {
|
||||||
|
return this.publishedTime.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
dehydrate(): DehydratedPostInfoProps {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
previewImageUrl: this.previewImageUrl.href,
|
||||||
|
labels: this.labels.map((label) => label.dehydrate()),
|
||||||
|
publishedTime: this.publishedTime.getTime()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DehydratedPostInfoProps {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
previewImageUrl: string;
|
||||||
|
labels: DehydratedLabelProps[];
|
||||||
|
publishedTime: number;
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,48 @@
|
|||||||
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||||
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||||
import type { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
import type { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
||||||
import { writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type PostListState = AsyncState<readonly PostInfoViewModel[]>;
|
||||||
|
export type PostListEvent = PostListLoadedEvent;
|
||||||
|
|
||||||
export class PostListBloc {
|
export class PostListBloc {
|
||||||
constructor(private readonly getAllPostsUseCase: GetAllPostUseCase) {}
|
private readonly state = writable<PostListState>({
|
||||||
|
|
||||||
private readonly state = writable<AsyncState<readonly PostInfoViewModel[]>>({
|
|
||||||
status: StatusType.Idle
|
status: StatusType.Idle
|
||||||
});
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly getAllPostsUseCase: GetAllPostsUseCase,
|
||||||
|
initialData?: readonly PostInfoViewModel[]
|
||||||
|
) {
|
||||||
|
this.state.set({
|
||||||
|
status: StatusType.Idle,
|
||||||
|
data: initialData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get subscribe() {
|
get subscribe() {
|
||||||
return this.state.subscribe;
|
return this.state.subscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(event: PostListEvent) {
|
async dispatch(event: PostListEvent): Promise<PostListState> {
|
||||||
switch (event.event) {
|
switch (event.event) {
|
||||||
case PostListEventType.PostListLoadedEvent:
|
case PostListEventType.PostListLoadedEvent:
|
||||||
this.loadPosts();
|
return this.loadPosts();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadPosts() {
|
private async loadPosts(): Promise<PostListState> {
|
||||||
this.state.set({ status: StatusType.Loading });
|
this.state.set({ status: StatusType.Loading, data: get(this.state).data });
|
||||||
const posts = await this.getAllPostsUseCase.execute();
|
const posts = await this.getAllPostsUseCase.execute();
|
||||||
const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post));
|
const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post));
|
||||||
this.state.set({
|
const result: PostListState = {
|
||||||
status: StatusType.Success,
|
status: StatusType.Success,
|
||||||
data: postViewModels
|
data: postViewModels
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this.state.set(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,5 +53,3 @@ export enum PostListEventType {
|
|||||||
export interface PostListLoadedEvent {
|
export interface PostListLoadedEvent {
|
||||||
event: PostListEventType.PostListLoadedEvent;
|
event: PostListEventType.PostListLoadedEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PostListEvent = PostListLoadedEvent;
|
|
||||||
|
47
frontend/src/lib/post/adapter/presenter/postViewModel.ts
Normal file
47
frontend/src/lib/post/adapter/presenter/postViewModel.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
PostInfoViewModel,
|
||||||
|
type DehydratedPostInfoProps
|
||||||
|
} from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||||
|
import type { Post } from '$lib/post/domain/entity/post';
|
||||||
|
|
||||||
|
export class PostViewModel {
|
||||||
|
id: number;
|
||||||
|
info: PostInfoViewModel;
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
private constructor(props: { id: number; info: PostInfoViewModel; content: string }) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.info = props.info;
|
||||||
|
this.content = props.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromEntity(post: Post): PostViewModel {
|
||||||
|
return new PostViewModel({
|
||||||
|
id: post.id,
|
||||||
|
info: PostInfoViewModel.fromEntity(post.info),
|
||||||
|
content: post.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static rehydrate(props: DehydratedPostProps): PostViewModel {
|
||||||
|
return new PostViewModel({
|
||||||
|
id: props.id,
|
||||||
|
info: PostInfoViewModel.rehydrate(props.info),
|
||||||
|
content: props.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dehydrate(): DehydratedPostProps {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
info: this.info.dehydrate(),
|
||||||
|
content: this.content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DehydratedPostProps {
|
||||||
|
id: number;
|
||||||
|
info: DehydratedPostInfoProps;
|
||||||
|
content: string;
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
|
import type { Post } from '$lib/post/domain/entity/post';
|
||||||
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
|
|
||||||
export interface PostRepository {
|
export interface PostRepository {
|
||||||
getAllPosts(): Promise<PostInfo[]>;
|
getAllPosts(): Promise<PostInfo[]>;
|
||||||
|
getPost(id: number): Promise<Post | null>;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { PostRepository } from '$lib/post/application/repository/postRepository';
|
import type { PostRepository } from '$lib/post/application/repository/postRepository';
|
||||||
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
|
|
||||||
export class GetAllPostUseCase {
|
export class GetAllPostsUseCase {
|
||||||
constructor(private readonly postRepository: PostRepository) {}
|
constructor(private readonly postRepository: PostRepository) {}
|
||||||
|
|
||||||
execute(): Promise<PostInfo[]> {
|
execute(): Promise<PostInfo[]> {
|
||||||
|
10
frontend/src/lib/post/application/useCase/getPostUseCase.ts
Normal file
10
frontend/src/lib/post/application/useCase/getPostUseCase.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { PostRepository } from '$lib/post/application/repository/postRepository';
|
||||||
|
import type { Post } from '$lib/post/domain/entity/post';
|
||||||
|
|
||||||
|
export class GetPostUseCase {
|
||||||
|
constructor(private readonly postRepository: PostRepository) {}
|
||||||
|
|
||||||
|
execute(id: number): Promise<Post | null> {
|
||||||
|
return this.postRepository.getPost(id);
|
||||||
|
}
|
||||||
|
}
|
13
frontend/src/lib/post/domain/entity/post.ts
Normal file
13
frontend/src/lib/post/domain/entity/post.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
|
|
||||||
|
export class Post {
|
||||||
|
id: number;
|
||||||
|
info: PostInfo;
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
constructor(props: { id: number; info: PostInfo; content: string }) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.info = props.info;
|
||||||
|
this.content = props.content;
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,15 @@
|
|||||||
import { Environment } from '$lib/environment';
|
import { Environment } from '$lib/environment';
|
||||||
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
||||||
import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto';
|
import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto';
|
||||||
|
import { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto';
|
||||||
|
|
||||||
export class PostApiServiceImpl implements PostApiService {
|
export class PostApiServiceImpl implements PostApiService {
|
||||||
|
constructor(private fetchFn: typeof fetch) {}
|
||||||
|
|
||||||
async getAllPosts(): Promise<PostInfoResponseDto[]> {
|
async getAllPosts(): Promise<PostInfoResponseDto[]> {
|
||||||
const url = new URL('post/all', Environment.API_BASE_URL);
|
const url = new URL('post/all', Environment.API_BASE_URL);
|
||||||
|
|
||||||
const response = await fetch(url.href);
|
const response = await this.fetchFn(url.href);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return [];
|
return [];
|
||||||
@ -15,4 +18,17 @@ export class PostApiServiceImpl implements PostApiService {
|
|||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
return json.map(PostInfoResponseDto.fromJson);
|
return json.map(PostInfoResponseDto.fromJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPost(id: number): Promise<PostResponseDto | null> {
|
||||||
|
const url = new URL(`post/${id}`, Environment.API_BASE_URL);
|
||||||
|
|
||||||
|
const response = await this.fetchFn(url.href);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return PostResponseDto.fromJson(json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
frontend/src/lib/post/framework/ui/Label.svelte
Normal file
13
frontend/src/lib/post/framework/ui/Label.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel';
|
||||||
|
|
||||||
|
const { label }: { label: LabelViewModel } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||||
|
style="background-color: {label.color.hex};"
|
||||||
|
>
|
||||||
|
<div class="size-2 rounded-full" style="background-color: {label.color.darken(0.2).hex};"></div>
|
||||||
|
<span class="text-xs">{label.name}</span>
|
||||||
|
</div>
|
17
frontend/src/lib/post/framework/ui/PostContentHeader.svelte
Normal file
17
frontend/src/lib/post/framework/ui/PostContentHeader.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||||
|
import Label from '$lib/post/framework/ui/Label.svelte';
|
||||||
|
|
||||||
|
const { postInfo }: { postInfo: PostInfoViewModel } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col pt-9 md:pt-20">
|
||||||
|
<div class="mb-4 flex flex-row gap-2">
|
||||||
|
{#each postInfo.labels as label (label.id)}
|
||||||
|
<Label {label} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-800 sm:text-4xl md:text-5xl">{postInfo.title}</h1>
|
||||||
|
<p class="max-w-3xl">{postInfo.description}</p>
|
||||||
|
<span class="text-gray-500">{postInfo.formattedPublishedTime}</span>
|
||||||
|
</div>
|
27
frontend/src/lib/post/framework/ui/PostContentPage.svelte
Normal file
27
frontend/src/lib/post/framework/ui/PostContentPage.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { PostBloc, PostEventType } from '$lib/post/adapter/presenter/postBloc';
|
||||||
|
import PostContentHeader from '$lib/post/framework/ui/PostContentHeader.svelte';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import markdownit from 'markdown-it';
|
||||||
|
import SafeHtml from '$lib/common/framework/ui/SafeHtml.svelte';
|
||||||
|
|
||||||
|
const { id }: { id: number } = $props();
|
||||||
|
|
||||||
|
const postBloc = getContext<PostBloc>(PostBloc.name);
|
||||||
|
const state = $derived($postBloc);
|
||||||
|
|
||||||
|
const md = markdownit();
|
||||||
|
const parsedContent = $derived(state.data?.content ? md.render(state.data.content) : '');
|
||||||
|
|
||||||
|
onMount(() => postBloc.dispatch({ event: PostEventType.PostLoadedEvent, id: id }));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article class="container prose pb-10">
|
||||||
|
{#if state.data}
|
||||||
|
<PostContentHeader postInfo={state.data.info} />
|
||||||
|
<div class="max-w-3xl">
|
||||||
|
<hr />
|
||||||
|
<SafeHtml html={parsedContent} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</article>
|
@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { StatusType } from '$lib/common/adapter/presenter/asyncState';
|
|
||||||
import { PostListBloc, PostListEventType } from '$lib/post/adapter/presenter/postListBloc';
|
import { PostListBloc, PostListEventType } from '$lib/post/adapter/presenter/postListBloc';
|
||||||
import PostPreview from '$lib/post/framework/ui/PostPreview.svelte';
|
import PostPreview from '$lib/post/framework/ui/PostPreview.svelte';
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
@ -10,13 +9,11 @@
|
|||||||
onMount(() => postListBloc.dispatch({ event: PostListEventType.PostListLoadedEvent }));
|
onMount(() => postListBloc.dispatch({ event: PostListEventType.PostListLoadedEvent }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container pb-10">
|
||||||
<div class="py-9 text-center text-3xl font-bold text-gray-800 md:py-20 md:text-5xl">文章</div>
|
<h1 class="py-9 text-center text-3xl font-bold text-gray-800 md:py-20 md:text-5xl">文章</h1>
|
||||||
{#if state.status === StatusType.Success}
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-y-8 lg:grid-cols-3">
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-y-8 lg:grid-cols-3">
|
{#each state.data ?? [] as postInfo (postInfo.id)}
|
||||||
{#each state.data as postInfo (postInfo.id)}
|
<PostPreview {postInfo} />
|
||||||
<PostPreview {postInfo} />
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a class="flex cursor-pointer flex-col gap-y-4" href="/post/{postInfo.id}">
|
<a class="flex cursor-pointer flex-col gap-y-6" href="/post/{postInfo.id}">
|
||||||
<div class="relative aspect-video overflow-hidden rounded-2xl bg-gray-200">
|
<div class="relative aspect-video overflow-hidden rounded-2xl bg-gray-200">
|
||||||
<img
|
<img
|
||||||
class="rounded-2xl object-cover transition-opacity duration-300
|
class="rounded-2xl object-cover transition-opacity duration-300
|
||||||
@ -32,10 +32,10 @@
|
|||||||
<div class="absolute inset-0 flex items-center justify-center bg-gray-200"></div>
|
<div class="absolute inset-0 flex items-center justify-center bg-gray-200"></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-y-1.5">
|
<div class="flex flex-col gap-y-2.5">
|
||||||
<PostPreviewLabels labels={postInfo.labels} />
|
<PostPreviewLabels labels={postInfo.labels} />
|
||||||
<span class="line-clamp-1 font-bold">{postInfo.title}</span>
|
<span class="line-clamp-1 text-lg font-bold">{postInfo.title}</span>
|
||||||
<span class="line-clamp-3 text-justify text-sm">{postInfo.description}</span>
|
<span class="line-clamp-3 text-justify text-sm">{postInfo.description}</span>
|
||||||
<span class="text-sm text-gray-500">查看更多 ⭢</span>
|
<span class="text-sm text-gray-500">{postInfo.formattedPublishedTime}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel';
|
import type { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel';
|
||||||
|
import Label from '$lib/post/framework/ui/Label.svelte';
|
||||||
|
|
||||||
const { labels }: { labels: readonly LabelViewModel[] } = $props();
|
const { labels }: { labels: readonly LabelViewModel[] } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row gap-x-2 text-xs">
|
<div class="flex flex-row gap-x-2">
|
||||||
{#each labels.slice(0, 2) as label (label.id)}
|
{#each labels.slice(0, 2) as label (label.id)}
|
||||||
<div
|
<Label {label} />
|
||||||
class="flex flex-row items-center gap-x-1 rounded-full px-2 py-0.5"
|
|
||||||
style="background-color: {label.color.hex};"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="size-2 rounded-full"
|
|
||||||
style="background-color: {label.color.darken(0.2).hex};"
|
|
||||||
></div>
|
|
||||||
<span>{label.name}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{#if labels.length > 2}
|
{#if labels.length > 2}
|
||||||
<div class="rounded-full bg-gray-200 px-2 py-0.5">
|
<div class="rounded-full bg-gray-200 px-2 py-0.5 text-xs">
|
||||||
<span>+{labels.length - 2}</span>
|
<span>+{labels.length - 2}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
12
frontend/src/routes/post/+page.server.ts
Normal file
12
frontend/src/routes/post/+page.server.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { PostListEventType } from '$lib/post/adapter/presenter/postListBloc';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
const { postListBloc } = locals;
|
||||||
|
|
||||||
|
const state = await postListBloc.dispatch({ event: PostListEventType.PostListLoadedEvent });
|
||||||
|
|
||||||
|
return {
|
||||||
|
dehydratedData: state.data?.map((post) => post.dehydrate())
|
||||||
|
};
|
||||||
|
};
|
@ -1,15 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl';
|
import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl';
|
||||||
import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc';
|
import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc';
|
||||||
import { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
import { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
||||||
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
|
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
|
||||||
import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte';
|
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||||
|
import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte';
|
||||||
|
|
||||||
const postApiService = new PostApiServiceImpl();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
|
const initialData = data.dehydratedData?.map((post) => PostInfoViewModel.rehydrate(post));
|
||||||
|
|
||||||
|
const postApiService = new PostApiServiceImpl(fetch);
|
||||||
const postRepository = new PostRepositoryImpl(postApiService);
|
const postRepository = new PostRepositoryImpl(postApiService);
|
||||||
const getAllPostsUseCase = new GetAllPostUseCase(postRepository);
|
const getAllPostsUseCase = new GetAllPostsUseCase(postRepository);
|
||||||
const postListBloc = new PostListBloc(getAllPostsUseCase);
|
const postListBloc = new PostListBloc(getAllPostsUseCase, initialData);
|
||||||
|
|
||||||
setContext(PostListBloc.name, postListBloc);
|
setContext(PostListBloc.name, postListBloc);
|
||||||
</script>
|
</script>
|
||||||
|
24
frontend/src/routes/post/[id]/+page.server.ts
Normal file
24
frontend/src/routes/post/[id]/+page.server.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { PostEventType } from '$lib/post/adapter/presenter/postBloc';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||||
|
const { postBloc } = locals;
|
||||||
|
|
||||||
|
const id = parseInt(params.id, 10);
|
||||||
|
if (isNaN(id) || id <= 0) {
|
||||||
|
error(400, { message: 'Invalid post ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await postBloc.dispatch({
|
||||||
|
event: PostEventType.PostLoadedEvent,
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
if (!state.data) {
|
||||||
|
error(404, { message: 'Post not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dehydratedData: state.data.dehydrate()
|
||||||
|
};
|
||||||
|
};
|
25
frontend/src/routes/post/[id]/+page.svelte
Normal file
25
frontend/src/routes/post/[id]/+page.svelte
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl';
|
||||||
|
import { PostBloc } from '$lib/post/adapter/presenter/postBloc';
|
||||||
|
import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
||||||
|
import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
|
||||||
|
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import PostContentPage from '$lib/post/framework/ui/PostContentPage.svelte';
|
||||||
|
|
||||||
|
const { data, params }: PageProps = $props();
|
||||||
|
|
||||||
|
const id = parseInt(params.id, 10);
|
||||||
|
|
||||||
|
const initialData = PostViewModel.rehydrate(data.dehydratedData!);
|
||||||
|
|
||||||
|
const postApiService = new PostApiServiceImpl(fetch);
|
||||||
|
const postRepository = new PostRepositoryImpl(postApiService);
|
||||||
|
const getPostUseCase = new GetPostUseCase(postRepository);
|
||||||
|
const postBloc = new PostBloc(getPostUseCase, initialData);
|
||||||
|
|
||||||
|
setContext(PostBloc.name, postBloc);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PostContentPage {id} />
|
Loading…
x
Reference in New Issue
Block a user