Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload avatar #167

Draft
wants to merge 38 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3721586
add avatar upload component
Jan 23, 2020
11e4796
schema auto change
Jan 23, 2020
9d0bbbd
add axios
Jan 23, 2020
02135d3
add change avatar mutation
Jan 23, 2020
156e92e
add cors. This is bad...I think
Jan 23, 2020
3644031
add aws env vars
Jan 23, 2020
5190ff5
add s3 api route
Jan 23, 2020
acaffb7
use avatarPhoto if exists
Jan 23, 2020
40c4ffb
yarn lock change
Jan 23, 2020
02ffa19
add avatar upload to settings page
Jan 23, 2020
882ab58
add labels to error, success, and avatar form item
Jan 23, 2020
594364c
replace readme with instructions
makoncline Jan 23, 2020
84e304f
add @app/graphql
makoncline Jan 27, 2020
1bb7b59
Extend schema to allow for presigned URLs for uploading files
singingwolfboy Feb 12, 2020
e96c5e1
respond to review comments
singingwolfboy Apr 18, 2020
c9378b3
update schema.graphql
singingwolfboy Apr 18, 2020
0309090
use typescript enum
singingwolfboy Apr 18, 2020
f28ebc2
prettier
singingwolfboy Apr 18, 2020
76807dc
missed a backtick
singingwolfboy Apr 18, 2020
62c7e83
Add dependency on aws-sdk to server/package.json
singingwolfboy Apr 20, 2020
8626b2f
respond to review comments
singingwolfboy Apr 20, 2020
8442139
remove unneeded query field from payload
singingwolfboy Apr 20, 2020
8b73401
process.env.AWS_BUCKET_UPLOAD
singingwolfboy Apr 20, 2020
7d3ff8b
resolve conflict
singingwolfboy May 9, 2020
75475d4
uuid 8
singingwolfboy May 9, 2020
d08e934
match aws-sdk versions
singingwolfboy May 9, 2020
2617eb2
Merge remote-tracking branch 'singingwolfboy/upload-via-signed-url-ex…
benjie May 15, 2020
d8b0314
Merge remote-tracking branch 'makoncline/master' into makoncline-upload
benjie May 15, 2020
544cee6
Lint fix
benjie May 15, 2020
b4c984b
Use 'slugify'
benjie May 15, 2020
5ff02f3
Merge branch 'makoncline-upload' into upload-avatar
benjie May 15, 2020
5a17463
Use UpdateUser mutation instead of ChangeAvatar mutation
benjie May 15, 2020
c14278d
Schema export script
benjie May 15, 2020
0fdc0ff
Start refactoring
benjie May 15, 2020
65620dd
Split independent forms
benjie May 15, 2020
c521d3d
More refactoring
benjie May 15, 2020
ee48e4c
awsRegion restored to behaviour on master
benjie May 15, 2020
fa15355
Merge branch 'master' into upload-avatar
benjie May 15, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions @app/client/src/graphql/CreateUploadUrl.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation CreateUploadUrl($input: CreateUploadUrlInput!) {
createUploadUrl(input: $input) {
uploadUrl
}
}
1 change: 1 addition & 0 deletions @app/client/src/graphql/UpdateUser.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mutation UpdateUser($id: UUID!, $patch: UserPatch!) {
id
name
username
avatarUrl
}
}
}
131 changes: 74 additions & 57 deletions @app/client/src/pages/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ErrorAlert, Redirect, SettingsLayout } from "@app/components";
import {
AvatarUpload,
ErrorAlert,
Redirect,
SettingsLayout,
} from "@app/components";
import {
ProfileSettingsForm_UserFragment,
useSettingsProfileQuery,
Expand All @@ -10,7 +15,7 @@ import {
getCodeFromError,
tailFormItemLayout,
} from "@app/lib";
import { Alert, Button, Form, Input, PageHeader } from "antd";
import { Alert, Button, Card, Form, Input, PageHeader } from "antd";
import { useForm } from "antd/lib/form/util";
import { ApolloError } from "apollo-client";
import { NextPage } from "next";
Expand Down Expand Up @@ -104,64 +109,76 @@ function ProfileSettingsForm({
const code = getCodeFromError(error);
return (
<div>
<PageHeader title="Edit profile" />
<Form
{...formItemLayout}
form={form}
onFinish={handleSubmit}
initialValues={{ name: user.name, username: user.username }}
>
<Form.Item
label="Name"
name="name"
rules={[
{
required: true,
message: "Please enter your name",
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Username"
name="username"
rules={[
{
required: true,
message: "Please choose a username",
},
]}
<PageHeader title="Profile" />
<Card title="Update profile information" style={{ margin: "1rem" }}>
<Form
{...formItemLayout}
form={form}
onFinish={handleSubmit}
initialValues={{ name: user.name, username: user.username }}
>
<Input />
</Form.Item>
{error ? (
<Form.Item>
<Alert
type="error"
message={`Updating username`}
description={
<span>
{extractError(error).message}
{code ? (
<span>
{" "}
(Error code: <code>ERR_{code}</code>)
</span>
) : null}
</span>
}
/>
<Form.Item
label="Name"
name="name"
rules={[
{
required: true,
message: "Please enter your name",
},
]}
>
<Input />
</Form.Item>
) : success ? (
<Form.Item>
<Alert type="success" message={`Profile updated`} />
<Form.Item
label="Username"
name="username"
rules={[
{
required: true,
message: "Please choose a username",
},
]}
>
<Input />
</Form.Item>
) : null}
<Form.Item {...tailFormItemLayout}>
<Button htmlType="submit">Update Profile</Button>
</Form.Item>
</Form>
{error ? (
<Form.Item {...tailFormItemLayout}>
<Alert
type="error"
message={`Updating username`}
description={
<span>
{extractError(error).message}
{code ? (
<span>
{" "}
(Error code: <code>ERR_{code}</code>)
</span>
) : null}
</span>
}
/>
</Form.Item>
) : success ? (
<Form.Item {...tailFormItemLayout}>
<Alert type="success" message={`Profile updated`} />
</Form.Item>
) : null}
<Form.Item {...tailFormItemLayout}>
<Button htmlType="submit">Update Profile</Button>
</Form.Item>
</Form>
</Card>
<Card title="Update avatar" style={{ margin: "1rem" }}>
<div
style={{
display: "flex",
justifyContent: "center",
}}
>
<AvatarUpload user={user} />
</div>
</Card>
</div>
);
}
3 changes: 3 additions & 0 deletions @app/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
"@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.5",
"@app/graphql": "0.0.0",
"@app/lib": "0.0.0",
"@types/axios": "^0.14.0",
"antd": "^4.2.0",
"apollo-client": "^2.6.8",
"axios": "^0.19.2",
"next": "^9.3.6",
"react": "^16.13.1",
"tslib": "^1.11.1"
Expand Down
158 changes: 158 additions & 0 deletions @app/components/src/AvatarUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { LoadingOutlined, PlusOutlined } from "@ant-design/icons";
import {
ProfileSettingsForm_UserFragment,
useCreateUploadUrlMutation,
useUpdateUserMutation,
} from "@app/graphql";
import { extractError, getExceptionFromError } from "@app/lib";
import { message, Upload } from "antd";
import {
RcCustomRequestOptions,
UploadChangeParam,
UploadFile,
} from "antd/lib/upload/interface";
import axios from "axios";
import React, { useState } from "react";
import slugify from "slugify";

export function getUid(name: string) {
const randomHex = () => Math.floor(Math.random() * 16777215).toString(16);
const fileNameSlug = slugify(name);
return randomHex() + "-" + fileNameSlug;
}

const ALLOWED_UPLOAD_CONTENT_TYPES = [
"image/apng",
"image/bmp",
"image/gif",
"image/jpeg",
"image/png",
"image/svg+xml",
"image/tiff",
"image/webp",
];
const ALLOWED_UPLOAD_CONTENT_TYPES_STRING = ALLOWED_UPLOAD_CONTENT_TYPES.join(
","
);

export function AvatarUpload({
user,
}: {
user: ProfileSettingsForm_UserFragment;
}) {
const [updateUser] = useUpdateUserMutation();

const beforeUpload = (file: any) => {
const fileName = file.name.split(".")[0];
const fileType = file.name.split(".")[1];
file.uid = getUid(fileName) + "." + fileType;
const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";
if (!isJpgOrPng) {
message.error("You can only upload JPG or PNG images!");
file.status = "error";
}
const isLt3M = file.size / 1024 / 1024 < 3;
if (!isLt3M) {
message.error("Image must smaller than 3MB!");
file.status = "error";
}
return isJpgOrPng && isLt3M;
};

const [createUploadUrl] = useCreateUploadUrlMutation();

const [loading, setLoading] = useState(false);

const customRequest = async (option: RcCustomRequestOptions) => {
const { onSuccess, onError, file, onProgress } = option;
try {
const contentType = file.type;
const { data } = await createUploadUrl({
variables: {
input: {
contentType,
},
},
});
const uploadUrl = data?.createUploadUrl?.uploadUrl;

if (!uploadUrl) {
throw new Error("Failed to generate upload URL");
}
const response = await axios.put(uploadUrl, file, {
onUploadProgress: (e) => {
const progress = Math.round((e.loaded / e.total) * 100);
onProgress({ percent: progress }, file);
},
});
if (response.config.url) {
await updateUser({
variables: {
id: user.id,
patch: {
avatarUrl: response.config.url.split("?")[0],
},
},
});
onSuccess(response.config, file);
}
} catch (e) {
console.error(e);
onError(e);
}
};

const uploadButton = (
<div>
{loading ? <LoadingOutlined /> : <PlusOutlined />}
<div className="ant-upload-text">Upload</div>
</div>
);

const onChange = (info: UploadChangeParam<UploadFile<any>>) => {
switch (info.file.status) {
case "uploading": {
setLoading(true);
break;
}
case "removed":
case "success": {
setLoading(false);
break;
}
case "error": {
const error: any = getExceptionFromError(info.file.error);
console.dir(error);
message.error(
typeof error === "string"
? error
: error?.message ??
"Unknown error occurred" +
(error?.code ? ` (${error.code})` : "")
);
setLoading(false);
break;
}
}
};

return (
<div>
<Upload
accept={ALLOWED_UPLOAD_CONTENT_TYPES_STRING}
name="avatar"
listType="picture-card"
showUploadList={false}
beforeUpload={beforeUpload}
customRequest={customRequest}
onChange={onChange}
>
{user.avatarUrl ? (
<img src={user.avatarUrl} alt="avatar" style={{ width: "100%" }} />
) : (
uploadButton
)}
</Upload>
</div>
);
}
5 changes: 2 additions & 3 deletions @app/components/src/SharedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useCallback } from "react";

import { ErrorAlert, H3, StandardWidth, Warn } from ".";
import { Redirect } from "./Redirect";
import { UserAvatar } from "./UserAvatar";

const { Header, Content, Footer } = Layout;
const { Text } = Typography;
Expand Down Expand Up @@ -243,9 +244,7 @@ export function SharedLayout({
data-cy="layout-dropdown-user"
style={{ whiteSpace: "nowrap" }}
>
<Avatar>
{(data.currentUser.name && data.currentUser.name[0]) || "?"}
</Avatar>
<UserAvatar user={data.currentUser} />
<Warn okay={data.currentUser.isVerified}>
<span style={{ marginLeft: 8, marginRight: 8 }}>
{data.currentUser.name}
Expand Down
16 changes: 16 additions & 0 deletions @app/components/src/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Avatar } from "antd";
import React, { FC } from "react";

export const UserAvatar: FC<{
user: {
name?: string | null;
avatarUrl?: string | null;
};
}> = (props) => {
const { name, avatarUrl } = props.user;
if (avatarUrl) {
return <Avatar src={avatarUrl} />;
} else {
return <Avatar>{(name && name[0]) || "?"}</Avatar>;
}
};
2 changes: 2 additions & 0 deletions @app/components/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./AvatarUpload";
export * from "./ButtonLink";
export * from "./ErrorAlert";
export * from "./ErrorOccurred";
Expand All @@ -11,5 +12,6 @@ export * from "./SocialLoginOptions";
export * from "./SpinPadded";
export * from "./StandardWidth";
export * from "./Text";
export * from "./UserAvatar";
export * from "./Warn";
export * from "./organizationHooks";
Loading