实践 100 个 React 应用挑战 —— 06 个人资料 ——

首先

这篇文章是为了参与@Sicut_study的【React应用挑战百本】系列并进行输出而写的。

    • 実装ルールや成果物の達成条件は元記事に従うものとします。

 

    元記事との差別点として、具体的に自分がどんな実装を行ったのか(と必要に応じて解説)を記載します。

我的目标是跟随Sicut_study的100天React教程,并学习100天React。

这篇原文在这里。

 

上一篇文章

 

请解释这个事情

创建能够生成个人资料卡的服务

规则

引自原文文章

    • 主要なライブラリやフレームワークはReactである必要がありますが、その他のツールやライブラリ(例: Redux, Next.js, Styled Componentsなど)を組み合わせて使用することは自由

 

    • TypeScriptを利用する

 

    要件をみたせばデザインなどは自由

达到要求

从原文中引用

    • ユーザーが名前、誕生日、電話番号を入力し、プロフィール写真をアップロードできる

 

    • アップロードされた画像がプレビューとして表示される

 

    ユーザーが生成したプロフィールカードをPNG形式でダウンロードできる

实施

本文将按照以下方针进行实施。

    • 各UIパーツをコンポーネント化する

 

    ダウンロード機能の実装にはhtml2canvasとjspdfを活用する

程式碼如下所示。

src/components/Label.tsxsrc/components/Label.tsx
/** @jsxImportSource @emotion/react */
import { css } from “@emotion/react”;

interface LabelProps {
htmlFor: string;
children: React.ReactNode;
}

const labelStyle = css`
margin-bottom: 5px;
font-weight: bold;
`;

const Label = ({ htmlFor, children }: LabelProps) => {
return (

);
};

export default Label;

src/components/Label.tsx

src/components/Label.tsx
/** @jsxImportSource @emotion/react */
import { css } from “@emotion/react”;

interface LabelProps {
htmlFor: string;
children: React.ReactNode;
}

const labelStyle = css`
margin-bottom: 5px;
font-weight: bold;
`;

const Label = ({ htmlFor, children }: LabelProps) => {
return (

);
};

export default Label;

src/components/InputField.tsxsrc/components/InputField.tsx
/** @jsxImportSource @emotion/react */
import { css } from “@emotion/react”;
import Label from “./Label”;

interface InputFieldProps {
label: string;
type: string;
name: string;
value: string;
onChange: (e: React.ChangeEvent) => void;
}

const inputStyle = css`
width: 100%;
padding: 8px 12px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;

&:focus {
border-color: #007bff;
outline: none;
}
`;

const InputField = ({
label,
type,
name,
value,
onChange,
}: InputFieldProps) => {
return (


);
};

export default InputField;
src/components/InputField.tsx
/** @jsxImportSource @emotion/react */
import { css } from “@emotion/react”;
import Label from “./Label”;

interface InputFieldProps {
label: string;
type: string;
name: string;
value: string;
onChange: (e: React.ChangeEvent) => void;
}

const inputStyle = css`
width: 100%;
padding: 8px 12px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;

&:focus {
border-color: #007bff;
outline: none;
}
`;

const InputField = ({ label, type, name, value, onChange }: InputFieldProps) => {
return (


);
};

export default InputField;

src/components/ImageUpload.tsxsrc/components/ImageUpload.tsx
/** @jsxImportSource @emotion/react */
import { useState } from “react”;
import { css } from “@emotion/react”;
import Label from “./Label”;

interface ImageUploadProps {
label: string;
onImageChange: (file: File | null) => void;
}

const containerStyle = css`
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
`;

const ImageUpload = ({ label, onImageChange }: ImageUploadProps) => {
const [previewUrl, setPreviewUrl] = useState(“”);

const handleImageChange = (e: React.ChangeEvent) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
setPreviewUrl(URL.createObjectURL(file));
onImageChange(file);
} else {
onImageChange(null);
}
};

return (


);
};

export default ImageUpload;

src/components/ImageUpload.tsx的内容如下:

src/components/ImageUpload.tsx
/** @jsxImportSource @emotion/react */
import { useState } from “react”;
import { css } from “@emotion/react”;
import Label from “./Label”;

interface ImageUploadProps {
label: string;
onImageChange: (file: File | null) => void;
}

const containerStyle = css`
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
`;

const ImageUpload = ({ label, onImageChange }: ImageUploadProps) => {
const [previewUrl, setPreviewUrl] = useState(“”);

const handleImageChange = (e: React.ChangeEvent) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
setPreviewUrl(URL.createObjectURL(file));
onImageChange(file);
} else {
onImageChange(null);
}
};

return (


);
};

export default ImageUpload;

src/components/ProfileCard.tsxsrc/components/ProfileCard.tsx
/** @jsxImportSource @emotion/react */
import { css } from “@emotion/react”;

interface ProfileCardProps {
name: string;
birthday: string;
phoneNumber: string;
photo: string;
}

const cardStyle = css`
padding: 20px;
width: 100%;
`;

const photoContainerStyle = css`
display: flex;
justify-content: center;
width: 100%;
`;

const photoStyle = css`
width: 250px;
height: 250px;
border-radius: 50%;
object-fit: cover;
`;

const textStyle = css`
text-align: center;
margin: 10px 0;
`;

const ProfileCard = ({
name,
birthday,
phoneNumber,
photo,
}: ProfileCardProps) => {
return (

{photo && Profile}

{name}

{birthday}

{phoneNumber}

);
};

export default ProfileCard;

src/App.tsxsrc/App.tsx
/** @jsxImportSource @emotion/react */
import { useState } from “react”;
import { css } from “@emotion/react”;
import InputField from “./components/InputField”;
import ImageUpload from “./components/ImageUpload”;
import ProfileCard from “./components/ProfileCard”;
import html2canvas from “html2canvas”;
import jsPDF from “jspdf”;

interface UserProfile {
name: string;
birthday: string;
phoneNumber: string;
photo: string;
}

const appContainerStyle = css`
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
max-width: 900px;
margin: 0 auto;
`;

const formContainerStyle = css`
width: 35%;
margin-right: 20px;
`;

const cardContainerStyle = css`
width: 45%;
height: 400px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
`;

const downloadButtonStyle = css`
background-color: #4caf50;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 4px;
width: 100%;
`;

const App = () => {
const [userProfile, setUserProfile] = useState({
name: “”,
birthday: “”,
phoneNumber: “”,
photo: “”,
});

const handleInputChange = (e: React.ChangeEvent) => {
setUserProfile({ …userProfile, [e.target.name]: e.target.value });
};

const handleImageChange = (file: File | null) => {
if (file) {
setUserProfile({ …userProfile, photo: URL.createObjectURL(file) });
} else {
setUserProfile({ …userProfile, photo: “” });
}
};

const handleDownload = async () => {
const card = document.getElementById(“profile-card”);
if (card) {
const canvas = await html2canvas(card);
const imgData = canvas.toDataURL(“image/png”);
const pdf = new jsPDF();

const imgWidth = 210;
const imgHeight = (canvas.height * imgWidth) / canvas.width;

pdf.addImage(imgData, “PNG”, 0, 0, imgWidth, imgHeight);
pdf.save(“profile-card.pdf”);
}
};

return (

);
};

export default App;

做好

完成的形式如下。
下载也能正常运行。

image.png
image.png

最后

随着时间的推移,实施变得越来越耗时。我在考虑下次是否引入UI库。

我将继续努力,以完成100次比赛为目标。希望能得到支持的朋友们能够关注我,非常欢迎。希望能获得点赞和收藏。

再见。

下一篇文章

 

广告
将在 10 秒后关闭
bannerAds