Skip to content

Commit 82199dd

Browse files
committed
docs: add post about RemixでファイルをアップロードしたりダウンロードできるWebアプリを作った
1 parent 4043b73 commit 82199dd

File tree

16 files changed

+306
-4
lines changed

16 files changed

+306
-4
lines changed

.contents/card-links.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,20 @@
5353
"title": "Biome",
5454
"description": "Format, lint, and more in a fraction of a second.",
5555
"image": "https://biomejs.dev/img/og.png?v=2"
56+
},
57+
"https://oriverk.dev/blog/20240109-publish-uploader-site/": {
58+
"title": "ファイルをアップロード・ダウンロードできるWebアプリを公開しました | oriverk.dev",
59+
"description": "",
60+
"image": "https://oriverk.dev/api/og/blog/20240109-publish-uploader-site.webp"
61+
},
62+
"https://zenn.dev/oriverk/articles/2914b8bff52476": {
63+
"title": "RemixでFirestore Storageへファイルアップロード機能を作る",
64+
"description": "",
65+
"image": "https://res.cloudinary.com/zenn/image/upload/s--tbra3LP4--/c_fit%2Cg_north_west%2Cl_text:notosansjp-medium.otf_55:Remix%25E3%2581%25A7Firestore%2520Storage%25E3%2581%25B8%25E3%2583%2595%25E3%2582%25A1%25E3%2582%25A4%25E3%2583%25AB%25E3%2582%25A2%25E3%2583%2583%25E3%2583%2597%25E3%2583%25AD%25E3%2583%25BC%25E3%2583%2589%25E6%25A9%259F%25E8%2583%25BD%25E3%2582%2592%25E4%25BD%259C%25E3%2582%258B%2Cw_1010%2Cx_90%2Cy_100/g_south_west%2Cl_text:notosansjp-medium.otf_37:oriverk%2Cx_203%2Cy_121/g_south_west%2Ch_90%2Cl_fetch:aHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2pWb1Vhc3I2TG1WZ2ZycnEtRlhNUVhya3VSdm85OWFMWkNtS2xEPXMyNTAtYw==%2Cr_max%2Cw_90%2Cx_87%2Cy_95/v1627283836/default/og-base-w1200-v2.png"
66+
},
67+
"https://github.com/oriverk/remix-uploader": {
68+
"title": "GitHub - oriverk/remix-uploader: web app to upload and download file",
69+
"description": "web app to upload and download file. Contribute to oriverk/remix-uploader development by creating an account on GitHub.",
70+
"image": "https://repository-images.githubusercontent.com/893686198/02d16ff4-cea4-4aa1-ba29-c9a649f0d30a"
5671
}
5772
}

src/content/blog/20240109-publish-uploader-site.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ published: true
1313

1414
趣味関係で開発・メンテナンスしている JS 製のツールを GoogleDive で配布していたのですが、代替サービスを探していました。探す中で作れそうだ・作ってみたいと思い、2022 年 8 月にサイトを公開しました。
1515

16-
自ツール配布のためだけのサイトであり、また悪意のあるファイルを他者に UL されると困るので、サイト URL と GitHub リポジトリは非公開です。
16+
自ツール配布のためだけのサイトなので、GitHub リポジトリは非公開です。
1717

1818
### どんなサービス
1919

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
create: '2024-10-27'
3+
update: '2024-10-27'
4+
title: 'React hooks'
5+
tags: [react]
6+
published: true
7+
---
8+
9+
only for memo
10+
11+
## React hooks
12+
13+
### useDetectAdBlock
14+
15+
```ts
16+
// [aruniverse/adblock-detect-react](https://github.com/aruniverse/adblock-detect-react/tree/master)
17+
import { useEffect, useState } from "react";
18+
19+
export const useDetectAdBlock = () => {
20+
const [adBlockDetected, setAdBlockDetected] = useState(false);
21+
22+
useEffect(() => {
23+
// grab a domain from https://github1s.com/gorhill/uBlock/blob/master/docs/tests/hostname-pool.js
24+
const url = "https://www3.doubleclick.net";
25+
fetch(url, {
26+
method: "HEAD",
27+
mode: "no-cors",
28+
cache: "no-store",
29+
})
30+
.then(({ redirected }) => {
31+
if (redirected) setAdBlockDetected(true);
32+
})
33+
.catch(() => {
34+
setAdBlockDetected(true);
35+
});
36+
}, []);
37+
38+
return adBlockDetected;
39+
};
40+
41+
```
42+
43+
### useCopyToClipboard
44+
45+
```ts
46+
import { useState } from "react";
47+
48+
type CopiedValue = string | null;
49+
type CopyFn = (text: string) => Promise<boolean>;
50+
51+
export function useCopyToClipboard(): [CopiedValue, CopyFn] {
52+
const [copiedText, setCopiedText] = useState<CopiedValue>(null);
53+
54+
const copy: CopyFn = async (text) => {
55+
if (!navigator?.clipboard) {
56+
console.warn("Clipboard not supported");
57+
return false;
58+
}
59+
60+
// Try to save to clipboard then save it in the state if worked
61+
try {
62+
await navigator.clipboard.writeText(text);
63+
setCopiedText(text);
64+
return true;
65+
} catch (error) {
66+
console.warn("Copy failed", error);
67+
setCopiedText(null);
68+
return false;
69+
}
70+
};
71+
72+
return [copiedText, copy];
73+
}
74+
75+
```
76+
77+
## crypto.ts
78+
79+
```ts
80+
// [フロントエンド開発者のためのハッシュ関数入門](https://zenn.dev/bosushi/articles/3353a1b28ef93b#5.-%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E3%81%A7%E3%81%AE%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E9%96%A2%E6%95%B0%E3%81%AE%E9%81%A9%E7%94%A8)
81+
82+
// 仮パスワードとハッシュ値を取得
83+
// const params = new URLSearchParams(window.location.search);
84+
// const temporaryPassword = params.get('tempPass');
85+
// const hashedPassword = params.get('hash');
86+
87+
// ハッシュ関数(SHA-256)を用いて仮パスワードをハッシュ化
88+
const encoder = new TextEncoder();
89+
export async function getHash(tempPass: string) {
90+
const msgUint8 = encoder.encode(tempPass);
91+
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
92+
const binaryArray = new Uint8Array(hashBuffer);
93+
const hashArray = Array.from(binaryArray);
94+
const hashedPass = hashArray
95+
.map((b) => b.toString(16).padStart(2, "0"))
96+
.join("");
97+
return hashedPass;
98+
}
99+
100+
/**
101+
* compare password; if true, same
102+
* @param temporaryPassword
103+
* @param hashedPassword
104+
* @returns
105+
*/
106+
export async function comparePassword(
107+
temporaryPassword: string,
108+
hashedPassword: string,
109+
) {
110+
const hashedTempPass = await getHash(temporaryPassword);
111+
return hashedTempPass === hashedPassword;
112+
}
113+
114+
// getHash(temporaryPassword).then((hashedTempPass) => {
115+
// // 仮パスワードのハッシュ値とURLから取得したハッシュ値を比較
116+
// if(hashedTempPass === hashedPassword) {
117+
// // 新しいパスワードの入力と更新処理を実行
118+
// const newPassword = prompt("新しいパスワードを入力してください:");
119+
// // updatePassword(newPassword); // updatePasswordはサーバーと通信してパスワードを更新する関数とする
120+
// } else {
121+
// alert("URLが無効です。");
122+
// }
123+
// });
124+
125+
```
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
title: RemixでファイルをアップロードしたりダウンロードできるWebアプリを作った
3+
create: "2024-12-13"
4+
update: "2024-12-13"
5+
tags: [remix, firebase, cloudrun]
6+
description: "React製のファイルアップロードサイトをRemixに変更しました"
7+
published: true
8+
---
9+
10+
2022年8月に公開したファイルのアップロード・ダウンロードができるReact製Webアプリを、Remix製に変更しました。下は当該サイトについて2024年1月に投稿したものです。
11+
12+
https://oriverk.dev/blog/20240109-publish-uploader-site/
13+
14+
※以降、UL=アップロード、DL=ダウンロードと省略します。
15+
16+
## サービスの機能
17+
18+
このWebアプリは、ユーザがファイルをULし、またDL出来る仕組みを提供します。ULにはユーザ登録が必要ですが、DLはユーザ登録を必要とせず、ULした人のIDを知っている人であれば誰でもアクセス可能です。
19+
20+
ただし、当Webアプリは私が趣味関係で開発・メンテナンスしているJS製ツールを頒布するために作成したものなので、ユーザ登録及びファイルアップロードは私以外には出来ない様に制限しています。
21+
22+
- ユーザ認証機能
23+
- 新規登録
24+
- FirebaseAuthenticationを用いて、メールアドレスとパスワードでユーザ登録
25+
- 登録ユーザに関するその他の情報はFirebaseFirestoreに保存される
26+
- ログイン・ログアウト
27+
- 登録済みユーザはログインしてファイルを管理可能
28+
- セッション管理
29+
- セッションは一定時間で期限切れとなり、再ログインを促す
30+
- ファイルアップロード機能
31+
- 認証要件
32+
- ULを行うためには、メールアドレスとパスワードを用いたユーザ登録とログインが必要
33+
- UL可能ファイル
34+
- 拡張子やファイルサイズに制限を設ける(`.zip`ファイルのみに制限
35+
- アップロード後の処理
36+
- ファイルはFirebaseStorageに保存される
37+
- ファイルのメタデータおよび説明文などはFirebaseFirestoreに保存される
38+
- ファイルダウンロード機能
39+
- 認証不要
40+
- DLはユーザ登録不要
41+
- ファイル管理機能
42+
- 個別管理
43+
- 各ユーザは自身がULしたファイルのみを一覧で確認し、説明文などを編集できる。
44+
- 削除機能
45+
- ユーザは自身がULしたファイルを論理削除できる
46+
47+
## 使用した技術
48+
49+
今まではファイルとその関連情報の送受信をクライアントサイドで行っていたので、Webアプリ作成にはReactの他に`Firebase Javascript SDK``react-firebase-hooks`を用い、FirebaseHostingにデプロイしていました。
50+
51+
- FW
52+
- React, TypeScript
53+
- [CSFrequency/react-firebase-hooks: React Hooks for Firebase.](https://github.com/CSFrequency/react-firebase-hooks)
54+
- [React Hook Form - Simple React forms validation](https://www.react-hook-form.com/)
55+
- [Zod | Documentation](https://zod.dev/)
56+
- [react-dropzone](https://react-dropzone.js.org/)
57+
- Style
58+
- TailwindCSS
59+
- Data & Hosting
60+
- [Firebase](https://firebase.google.com)
61+
- Hosting, Firestore Database, Storage
62+
63+
今回はRemixを用い、データの送受信をサーバサイドで行うことにしました。また、アカウント登録やファイルアップロード時のフォームバリデーションには、`conform``zod` を利用しました。
64+
65+
- FW
66+
- React@18
67+
- [Remix@2](https://remix.run/)
68+
- Style
69+
- [tailwindcss](https://tailwindcss.com/)
70+
- [daisyUI](https://daisyui.com/)
71+
- Validation:
72+
- [Conform](https://conform.guide/)
73+
- [Zod](https://zod.dev/)
74+
- Data: Firebase Admin SDK
75+
- Hosting: [Cloud Run](https://cloud.google.com/run)
76+
77+
プログラムなどはGitHubレポジトリを参照
78+
79+
https://github.com/oriverk/remix-uploader
80+
81+
Remixを利用したFirebaseStorageへファイルをアップロードするプログラムについては、Zennに投稿しました。
82+
83+
https://zenn.dev/oriverk/articles/2914b8bff52476
84+
85+
## スクリーンショット
86+
87+
スクリーンショット画像中のアカウントはデモ用アカウントですが、各UL済みファイルは実際に存在する私のアカウントがULしたファイルを名前を変えて表示しています。DL数も2024年12月13日時点の実際の数字です。
88+
89+
<details>
90+
<summary>トップページ:/</summary>
91+
92+
![image](./103314.png)
93+
94+
</details>
95+
96+
<details>
97+
<summary>ファイル一覧ページ:/\{userId\}</summary>
98+
99+
![image](./124015.png)
100+
101+
</details>
102+
103+
<details>
104+
<summary>ユーザ登録ページ:/join</summary>
105+
106+
![image](./103225.png)
107+
108+
</details>
109+
110+
<details>
111+
<summary>ログインページ:/login</summary>
112+
113+
![image](./103212.png)
114+
115+
</details>
116+
117+
<details>
118+
<summary>ファイル管理ページ:/dashboard</summary>
119+
120+
![image](./104038.png)
121+
122+
</details>
123+
124+
<details>
125+
<summary>ファイル削除ダイアログ:/dashboard</summary>
126+
127+
![image](./104129.png)
128+
129+
</details>
130+
131+
<details>
132+
<summary>ファイルアップロードページ:/files/new</summary>
133+
134+
![image](./103340.png)
135+
136+
</details>
137+
138+
<details>
139+
<summary>ファイル編集ページ:/files/\{fileId\}/edit</summary>
140+
141+
![image](./104046.png)
142+
143+
</details>
144+
145+
## おわりに
146+
147+
ユーザ登録とファイルアップロード可能なユーザを私一人に限定していると言う理由で、いくつかさぼっている所があるので、適宜修正していきたい
148+
149+
- TODO
150+
- スタイルの修正
151+
- アカウント削除機能の追加
152+
- ファイルの物理削除を可能にする
153+
- 現在は論理削除のみ
154+
- コードの統一化及び整理
155+
156+
また、Remixへの移行作業の途中で、Reactは@19に、Remixは[ReactRouter@7](https://reactrouter.com/)になってしまいました。Remixに移行したばかりであるが、それらに移行したいと思っています。
157+
158+
![image](./150816.png "配布JSツールのDL数")
159+
160+
頒布しているJS製ツールは1000人ほどが使っている様です。自分が楽するために作ったモノをついでに配布しているだけなので、`Cloud Run`などの費用が赤字にならなさえすれば、まあ十分です。

src/content/blog/markdown-guide.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ ja-nn
107107
in MDX v2, bare link and link with `<>` is completely deprecated to avoid `<>` as jsx component.
108108

109109
```plaintext
110-
https://ixanary.com
110+
https://google.com
111111
```
112112

113113
## 画像

src/pages/api/og/[...path].ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export function getOgImageSrc(origin: string, pathname: string) {
2424
export const getStaticPaths = (async () => {
2525
const blogCollection = await getCollection("blog");
2626
const results = blogCollection.map((post) => {
27-
const { collection, slug, data } = post;
28-
const path = `${collection}/${slug}.${extension}`;
27+
const { collection, id, data } = post;
28+
const path = `${collection}/${id}.${extension}`;
2929
return {
3030
params: { path },
3131
props: {

src/styles/markdown.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@
9797

9898
& img {
9999
max-width: 100%;
100+
object-fit: scale-down;
101+
max-height: 500px;
100102
}
101103

102104
& :not(pre) > code::before,

0 commit comments

Comments
 (0)