Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions api/lib/controllers/avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const Avatar = require("../models/avatar");

module.exports = {
get: async (req, res) => {
const subject_id = req.credentials.sub;
const avatar = await Avatar.findOne({ owner_id: subject_id });

res.status(200).send(avatar);
},

create: async (req, res) => {
const subject_username = req.credentials.user.username;
const subject_id = req.credentials.sub;

const getColor = () => {
return (
"hsl(" +
360 * Math.random() +
"," +
'100%,50%)'
);
};

const avatar = await Avatar.create({
color: getColor(),
user_id: subject_id
});

res.status(200).send(avatar);
},
};
25 changes: 25 additions & 0 deletions api/lib/models/avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const colorValidator = (v) => (/^#([0-9a-f]{3}){1,2}$/i).test(v)

const avatarSchema = new mongoose.Schema(
{
color: {
type: String,
validator: [colorValidator, 'Invalid color'],
required: true,
},
image: {
type: String
},
user_id: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
index: true,
}
}
);

module.exports = mongoose.model("Avatar", avatarSchema);
10 changes: 10 additions & 0 deletions api/lib/routes/avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const router = require("express").Router();
const avatarController = require("../controllers/avatar");
const { wrapController } = require("../util/error-wrapper");

const wrapped = wrapController(avatarController);

router.post("/", wrapped.create);
router.get("/", wrapped.get);

module.exports = router;
2 changes: 2 additions & 0 deletions api/lib/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const participant = require("./participant");
const takeaway = require("./takeaway");
const actionItem = require("./action-item");
const pubkey = require("./pubkey");
const avatar = require("./avatar")

router.use("/health", health);
router.use("/user", reqLogger, user);
Expand All @@ -26,5 +27,6 @@ router.use("/group", reqLogger, authMdw, group);
router.use("/invite", reqLogger, authMdw, invite);
router.use("/tag", reqLogger, authMdw, tag);
router.use("/pubkey", reqLogger, pubkey);
router.use("/avatar", reqLogger, authMdw, avatar)

module.exports = router;
8 changes: 8 additions & 0 deletions api/test/fakes/avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const ObjectID = require("mongoose").Types.ObjectId;

module.exports = () => {
return {
color: "hsl(144.61708086985226,77.63562668242602%,91.11721900382429%)",
user_id: new ObjectID()
};
};
57 changes: 57 additions & 0 deletions api/test/tests/controllers/avatar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { assert } = require("chai");
const dbUtils = require("../../utils/db");
const db = require("../../../lib/db");
const api = require("../../utils/api");
const client = require("../../utils/client");
const fakeAvatar = require("../../fakes/avatar");
const Avatar = require("../../../lib/models/avatar");

describe("lib/controllers/ui", () => {
let user_id;
const path = "/avatar";

before(async () => {
await api.start();
await db.connect();
await dbUtils.clean();

const res = await client.post("/user/register", {
username: "user",
password: "pass",
email: "email",
});

user_id = res.data.user._id;

client.defaults.headers.common["Authorization"] = res.data.access_token;
});

after(async () => {
await api.stop();
await db.disconnect();
});

describe("#get", () => {
it("should get a users avatar", async () => {
const fake = fakeAvatar()
await Avatar.create({...fake, user_id: user_id});

const res = await client.get(path);

assert.strictEqual(res.status, 200);
assert.strictEqual(res.data.user_id, user_id);
});
});

describe("#create", () => {
it("should create a new Avatar", async () => {
const res = await client.post(path, {});

assert.strictEqual(res.status, 200);

const inserted = await Avatar.findOne({ user_id });

assert.strictEqual(inserted.user_id.toString(), user_id);
});
});
});
2 changes: 1 addition & 1 deletion scripts/autoPopulate.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const TEST_EMAIL = 'test@test.com';
/**
* Get rand int
*
* @param max - max number
* @param max - max number
*
* @returns {Number}
*/
Expand Down
11 changes: 11 additions & 0 deletions www/components/Layout/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { useRouter } from "next/router";
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { refreshTheme } from "../../store/features/theme";
import { refreshAvatar, createAvatar } from "../../store/features/avatar";

import styles from "./Layout.module.css";

const pagesWithoutDrawer = new Set(["", "login", "register"]);

const Layout = (props) => {
const user = useSelector((s) => s.user);
const avatar = useSelector((s) => s.avatar);
const drawerOpen = useSelector((s) => s.drawer);

const router = useRouter();
Expand All @@ -24,7 +26,16 @@ const Layout = (props) => {
props.store.dispatch(refreshTheme());
}

async function avatarRefresh() {
if (!avatar) {
props.store.dispatch(createAvatar());
return;
}
props.store.dispatch(refreshAvatar());
}

themeRefresh();
avatarRefresh();
}, [user]);

return (
Expand Down
2 changes: 1 addition & 1 deletion www/components/Layout/Nav/Nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const Nav = () => {
</IconButton>
</div>
<div className={styles.end}>
<ProfileButton />
<ProfileButton user={user} />
</div>
</nav>
</>
Expand Down
52 changes: 52 additions & 0 deletions www/components/Layout/ProfileButton/AccountModal/AccountModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Modal, Avatar } from "@mui/material";

@thudsonbu thudsonbu Dec 1, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it could be a good idea to create a separate account page at user/[_id] (with largely the same format as this modal). That should make it easier for us to allow group members to look at other user's accounts.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this initially there was just so much white space I couldn't make it look good. If you think well have enough content to give it its own page we could certainly do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the groups and integrations related things on the page we should have enough content.

import { CameraAlt } from "@mui/icons-material";
import styles from "./AccountModal.module.scss";
import Link from "next/link";

import { useSelector } from "react-redux";

export default function AccountModal({ open, setOpen, user }) {
const avatar = useSelector((s) => s.avatar);

return (
<Modal open={open} onClose={() => setOpen(false)}>
<div className={styles.modal_container}>
<section className={styles.main_options}>
<div className={styles.avatar_container}>
<Avatar
sx={{ bgcolor: avatar.color, width: 100, height: 100 }}
className={styles.avatar_current}
>
{avatar.initials}
{/* <input //Will need this for image upload in future
type="file"
className={styles.file_upload}
accept="image/*"
onChange={handleUpload}
/> */}
</Avatar>
<Avatar
className={styles.avatar_change}
sx={{ width: 100, height: 100 }}
>
<CameraAlt fontSize="large" color="primary" />
</Avatar>
</div>
<div className={styles.info}>
<h1>{user.username}</h1>
<span>{user.email}</span>
</div>
<div className={styles.account_options}>
<h3>Account Options:</h3>
<Link href="#">
<a>Change password</a>
</Link>
<Link href="#">
<a>Delete account</a>
</Link>
</div>
</section>
</div>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
.modal_container {
background: #fff;
border-radius: 6px;
width: 800px;
max-width: 90%;
min-width: 600px;
position: absolute;
top: 300px;
left: 50%;
transform: translate(-50%, -50%);
padding: 2em 2em 2em 2em;

&:focus {
border: none;
}
}

.main_options {
display: flex;
margin-right: 0;

.avatar_current:hover {
opacity: 20%;
}

.avatar_change{
position: absolute;
transform: translate(0, -100%);
z-index: -1;
}

.file_upload {
opacity: 0.0;

/* IE 8 */
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";

/* IE 5-7 */
filter: alpha(opacity=0);

/* Netscape or and older firefox browsers */
-moz-opacity: 0.0;

/* older Safari browsers */
-khtml-opacity: 0.0;

position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height:100%;
cursor: pointer;
}

.info{
display: flex;
flex-direction: column;
margin: 0 0 0 1em;
justify-content: center;


h1 {
font-weight: 500;
margin: 0;
}

span {
font-size: small;
}
}
}

.account_options {
display: flex;
flex-direction: column;
justify-content: center;
gap: .3em;
min-width: 125px;
margin-left: 25%;


a {
font-size: x-small;
}

a:hover {
opacity: 60%;
}
}
17 changes: 15 additions & 2 deletions www/components/Layout/ProfileButton/ProfileButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
MenuList,
IconButton,
} from "@mui/material";
import { AccountCircle, Logout } from "@mui/icons-material";
import { AccountCircle, Settings, Logout } from "@mui/icons-material";
import { useState } from "react";
import { useDispatch } from "react-redux";
import { userLogout } from "../../../store/features/user";
import AccountModal from "./AccountModal/AccountModal";

export default function ProfileButton() {
export default function ProfileButton({ user }) {
const [anchor, setAnchor] = useState(null);
const [openAccountModal, setOpenAccountModal] = useState(false);
const dispatch = useDispatch();

return (
Expand All @@ -26,6 +28,12 @@ export default function ProfileButton() {
</IconButton>
<Menu anchorEl={anchor} open={!!anchor} onClose={() => setAnchor(null)}>
<MenuList dense>
<MenuItem onClick={() => setOpenAccountModal(true)}>
<ListItemIcon>
<Settings />
</ListItemIcon>
<ListItemText inset>Account (Beta)</ListItemText>
</MenuItem>
<MenuItem onClick={() => dispatch(userLogout())}>
<ListItemIcon>
<Logout />
Expand All @@ -34,6 +42,11 @@ export default function ProfileButton() {
</MenuItem>
</MenuList>
</Menu>
<AccountModal
open={openAccountModal}
setOpen={setOpenAccountModal}
user={user}
/>
</>
);
}
Loading