mirror of
https://github.com/rjNemo/devbook_ts
synced 2026-06-11 21:16:45 +00:00
refactor: Profile folder
This commit is contained in:
parent
1512e1a20f
commit
829668c00f
10 changed files with 288 additions and 239 deletions
|
|
@ -1,239 +0,0 @@
|
||||||
import React, {FC} from 'react';
|
|
||||||
// Redux
|
|
||||||
import {compose} from '@reduxjs/toolkit';
|
|
||||||
import {firestoreConnect} from 'react-redux-firebase';
|
|
||||||
import {connect} from 'react-redux';
|
|
||||||
import {RootState} from '../store';
|
|
||||||
// Routing
|
|
||||||
import {Link, useParams} from 'react-router-dom';
|
|
||||||
import Routes from '../constants/routes';
|
|
||||||
import NotFound from './NotFound';
|
|
||||||
// Style
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
|
||||||
import {
|
|
||||||
faGithub,
|
|
||||||
faFacebook,
|
|
||||||
faInstagram,
|
|
||||||
faLinkedin,
|
|
||||||
faTwitter,
|
|
||||||
faYoutube,
|
|
||||||
} from '@fortawesome/free-brands-svg-icons';
|
|
||||||
import {
|
|
||||||
faGlobe,
|
|
||||||
IconDefinition,
|
|
||||||
faCheck,
|
|
||||||
faStar,
|
|
||||||
faEye,
|
|
||||||
faCodeBranch,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
// Typing
|
|
||||||
import IDev, {getDescription} from '../models/Dev';
|
|
||||||
import Experience from '../types/Experience';
|
|
||||||
import {getTimePeriod} from '../types/TimePeriod';
|
|
||||||
import Education from '../types/Education';
|
|
||||||
import Repo from '../types/Repo';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
dev: IDev;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dev personal profile as seen by other people.
|
|
||||||
*/
|
|
||||||
const Profile: FC<IProps> = ({dev}) => {
|
|
||||||
// display 404 page if dev is null
|
|
||||||
if (dev === null) {
|
|
||||||
return <NotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** return the icon corresponding to the social name */
|
|
||||||
const renderSocialIcon = (name: string): IconDefinition => {
|
|
||||||
switch (name) {
|
|
||||||
case 'facebook':
|
|
||||||
return faFacebook;
|
|
||||||
case 'github':
|
|
||||||
return faGithub;
|
|
||||||
case 'instagram':
|
|
||||||
return faInstagram;
|
|
||||||
case 'linkedin':
|
|
||||||
return faLinkedin;
|
|
||||||
case 'twitter':
|
|
||||||
return faTwitter;
|
|
||||||
case 'youtube':
|
|
||||||
return faYoutube;
|
|
||||||
default:
|
|
||||||
return faGlobe;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return dev === undefined ? (
|
|
||||||
<div>Loading ... </div>
|
|
||||||
) : (
|
|
||||||
<section className="container">
|
|
||||||
<Link to={Routes.DEVELOPERS} className="btn">
|
|
||||||
Back to profiles
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="profile-grid my-1">
|
|
||||||
<div className="profile-top bg-primary p-2">
|
|
||||||
<img
|
|
||||||
src={dev.avatarUrl}
|
|
||||||
alt={dev.displayName}
|
|
||||||
className="round-img my-1"
|
|
||||||
/>
|
|
||||||
<h1 className="large">{dev.displayName}</h1>
|
|
||||||
<p className="lead">{getDescription(dev.status, dev.company)}</p>
|
|
||||||
<p>{dev.location}</p>
|
|
||||||
<div className="icons my-1">
|
|
||||||
{Object.entries(dev.links)
|
|
||||||
.sort()
|
|
||||||
.map(([icon, webAddress], i: number) => (
|
|
||||||
<a
|
|
||||||
href={webAddress}
|
|
||||||
key={i}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={renderSocialIcon(icon)} size="2x" />
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-about bg-light p-2">
|
|
||||||
<h2 className="text-primary">{`${dev.displayName}'s Bio`}</h2>
|
|
||||||
<p>
|
|
||||||
{dev.bio.length === 0
|
|
||||||
? 'Add a short bio to present yourself!'
|
|
||||||
: dev.bio}
|
|
||||||
</p>
|
|
||||||
<div className="line"></div>
|
|
||||||
<h2 className="text-primary">Skill Set</h2>
|
|
||||||
<div className="skills">
|
|
||||||
{dev.skills.length === 0
|
|
||||||
? 'Let us know about your skills!'
|
|
||||||
: dev.skills?.map((s: string, i: number) => (
|
|
||||||
<div className="p-1" key={i}>
|
|
||||||
<FontAwesomeIcon icon={faCheck} /> {s}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-exp bg-white p-2">
|
|
||||||
<h2 className="text-primary">Experiences</h2>
|
|
||||||
{dev.experiences.length === 0 ? (
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
src={require('../static/img/404.jpg')}
|
|
||||||
alt="no experiences"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
dev.experiences.map((exp: Experience, i: number) => (
|
|
||||||
<div key={i}>
|
|
||||||
<h3>{exp.company}</h3>
|
|
||||||
<p>{getTimePeriod(exp.from, exp.to)}</p>
|
|
||||||
<p>
|
|
||||||
<strong>Position: </strong>
|
|
||||||
{exp.position}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Description: </strong>
|
|
||||||
{exp.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-edu bg-white p-2">
|
|
||||||
<h2 className="text-primary">Education</h2>
|
|
||||||
{dev.educations.length === 0 ? (
|
|
||||||
<div>
|
|
||||||
<img src={require('../static/img/404.jpg')} alt="no educations" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
dev.educations.map((edu: Education, i: number) => (
|
|
||||||
<div key={i}>
|
|
||||||
<h3>{edu.school}</h3>
|
|
||||||
<p>{getTimePeriod(edu.from, edu.to)}</p>
|
|
||||||
<p>
|
|
||||||
<strong>Degree: </strong>
|
|
||||||
{edu.degree}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Field: </strong>
|
|
||||||
{edu.field}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Description: </strong>
|
|
||||||
{edu.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-github">
|
|
||||||
<h2 className="text-primary my-1">
|
|
||||||
<FontAwesomeIcon icon={faGithub} /> GitHub Repos
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{dev.repos?.length === 0 ? (
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
src={require('../static/img/404.jpg')}
|
|
||||||
alt="no repositories"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
dev.repos.map((r: Repo, i: number) => (
|
|
||||||
<div className="repo bg-white my-1 p-1" key={i}>
|
|
||||||
<div>
|
|
||||||
<h4>
|
|
||||||
<a href={r.url} target="_blank" rel="noopener noreferrer">
|
|
||||||
{r.name}
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
<p>{r.description}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ul>
|
|
||||||
<li className="badge badge-primary">
|
|
||||||
<FontAwesomeIcon icon={faStar} /> Stars: {r.stars}
|
|
||||||
</li>
|
|
||||||
<li className="badge badge-dark">
|
|
||||||
<FontAwesomeIcon icon={faEye} /> Watchers: {r.watchers}
|
|
||||||
</li>
|
|
||||||
<li className="badge badge-light">
|
|
||||||
<FontAwesomeIcon icon={faCodeBranch} /> Forks: {r.forks}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Container to fetch id params from thr URI and pass it to Profile page
|
|
||||||
*/
|
|
||||||
const ProfileContainer: FC = () => {
|
|
||||||
const {id} = useParams();
|
|
||||||
|
|
||||||
const Component = compose<FC>(
|
|
||||||
firestoreConnect(() => [`users/${id}`]),
|
|
||||||
connect(({firestore: {data}}: RootState) => ({
|
|
||||||
dev: data.users && data.users[id],
|
|
||||||
})),
|
|
||||||
)(Profile);
|
|
||||||
|
|
||||||
return <Component />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileContainer;
|
|
||||||
26
src/pages/Profile/About.tsx
Normal file
26
src/pages/Profile/About.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, {FC} from 'react';
|
||||||
|
// Styling
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
|
import {faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import IDev from '../../models/Dev';
|
||||||
|
|
||||||
|
const ProfileAbout: FC<IDev> = ({displayName, bio, skills}) => (
|
||||||
|
<div className="profile-about bg-light p-2">
|
||||||
|
<h2 className="text-primary">{`${displayName}'s Bio`}</h2>
|
||||||
|
<p>{bio.length === 0 ? 'Add a short bio to present yourself!' : bio}</p>
|
||||||
|
<div className="line"></div>
|
||||||
|
<h2 className="text-primary">Skill Set</h2>
|
||||||
|
<div className="skills">
|
||||||
|
{skills.length === 0
|
||||||
|
? 'Let us know about your skills!'
|
||||||
|
: skills?.map((s: string, i: number) => (
|
||||||
|
<div className="p-1" key={i}>
|
||||||
|
<FontAwesomeIcon icon={faCheck} /> {s}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default React.memo(ProfileAbout);
|
||||||
38
src/pages/Profile/Education.tsx
Normal file
38
src/pages/Profile/Education.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React, {FC} from 'react';
|
||||||
|
|
||||||
|
import Education from '../../types/Education';
|
||||||
|
import {getTimePeriod} from '../../types/TimePeriod';
|
||||||
|
|
||||||
|
import educationPicture from '../../static/img/education.svg';
|
||||||
|
|
||||||
|
const ProfileEducation: FC<{educations: Education[]}> = ({educations}) => (
|
||||||
|
<div className="profile-edu bg-white p-2">
|
||||||
|
<h2 className="text-primary">Education</h2>
|
||||||
|
{educations.length === 0 ? (
|
||||||
|
<div>
|
||||||
|
<img src={educationPicture} alt="no educations" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
educations.map((edu: Education, i: number) => (
|
||||||
|
<div key={i}>
|
||||||
|
<h3>{edu.school}</h3>
|
||||||
|
<p>{getTimePeriod(edu.from, edu.to)}</p>
|
||||||
|
<p>
|
||||||
|
<strong>Degree: </strong>
|
||||||
|
{edu.degree}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Field: </strong>
|
||||||
|
{edu.field}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Description: </strong>
|
||||||
|
{edu.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default React.memo(ProfileEducation);
|
||||||
34
src/pages/Profile/Experience.tsx
Normal file
34
src/pages/Profile/Experience.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React, {FC} from 'react';
|
||||||
|
|
||||||
|
import Experience from '../../types/Experience';
|
||||||
|
import {getTimePeriod} from '../../types/TimePeriod';
|
||||||
|
|
||||||
|
import expPicture from '../../static/img/experience.svg';
|
||||||
|
|
||||||
|
const ProfileExperience: FC<{experiences: Experience[]}> = ({experiences}) => (
|
||||||
|
<div className="profile-exp bg-white p-2">
|
||||||
|
<h2 className="text-primary">Experiences</h2>
|
||||||
|
{experiences.length === 0 ? (
|
||||||
|
<div>
|
||||||
|
<img src={expPicture} alt="no experiences" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
experiences.map((exp: Experience, i: number) => (
|
||||||
|
<div key={i}>
|
||||||
|
<h3>{exp.company}</h3>
|
||||||
|
<p>{getTimePeriod(exp.from, exp.to)}</p>
|
||||||
|
<p>
|
||||||
|
<strong>Position: </strong>
|
||||||
|
{exp.position}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Description: </strong>
|
||||||
|
{exp.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ProfileExperience;
|
||||||
50
src/pages/Profile/Github.tsx
Normal file
50
src/pages/Profile/Github.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React, {FC} from 'react';
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
|
import {faGithub} from '@fortawesome/free-brands-svg-icons';
|
||||||
|
import {faStar, faEye, faCodeBranch} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import Repo from '../../types/Repo';
|
||||||
|
|
||||||
|
import githubPicture from '../../static/img/github.svg';
|
||||||
|
|
||||||
|
const ProfileGithub: FC<{repos: Repo[]}> = ({repos}) => (
|
||||||
|
<div className="profile-github">
|
||||||
|
<h2 className="text-primary my-1">
|
||||||
|
<FontAwesomeIcon icon={faGithub} /> GitHub Repos
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{repos?.length === 0 ? (
|
||||||
|
<div>
|
||||||
|
<img src={githubPicture} alt="no repositories" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
repos.map((r: Repo, i: number) => (
|
||||||
|
<div className="repo bg-white my-1 p-1" key={i}>
|
||||||
|
<div>
|
||||||
|
<h4>
|
||||||
|
<a href={r.url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{r.name}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<p>{r.description}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li className="badge badge-primary">
|
||||||
|
<FontAwesomeIcon icon={faStar} /> Stars: {r.stars}
|
||||||
|
</li>
|
||||||
|
<li className="badge badge-dark">
|
||||||
|
<FontAwesomeIcon icon={faEye} /> Watchers: {r.watchers}
|
||||||
|
</li>
|
||||||
|
<li className="badge badge-light">
|
||||||
|
<FontAwesomeIcon icon={faCodeBranch} /> Forks: {r.forks}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default React.memo(ProfileGithub);
|
||||||
68
src/pages/Profile/Top.tsx
Normal file
68
src/pages/Profile/Top.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, {FC} from 'react';
|
||||||
|
// Styling
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
|
import {IconDefinition} from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import {
|
||||||
|
faFacebook,
|
||||||
|
faGithub,
|
||||||
|
faInstagram,
|
||||||
|
faLinkedin,
|
||||||
|
faTwitter,
|
||||||
|
faYoutube,
|
||||||
|
} from '@fortawesome/free-brands-svg-icons';
|
||||||
|
import {faGlobe} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import IDev, {getDescription} from '../../models/Dev';
|
||||||
|
|
||||||
|
const ProfileTop: FC<IDev> = ({
|
||||||
|
avatarUrl,
|
||||||
|
displayName,
|
||||||
|
status,
|
||||||
|
company,
|
||||||
|
links,
|
||||||
|
location,
|
||||||
|
}) => {
|
||||||
|
/** return the icon corresponding to the social name */
|
||||||
|
const renderSocialIcon = (name: string): IconDefinition => {
|
||||||
|
switch (name) {
|
||||||
|
case 'facebook':
|
||||||
|
return faFacebook;
|
||||||
|
case 'github':
|
||||||
|
return faGithub;
|
||||||
|
case 'instagram':
|
||||||
|
return faInstagram;
|
||||||
|
case 'linkedin':
|
||||||
|
return faLinkedin;
|
||||||
|
case 'twitter':
|
||||||
|
return faTwitter;
|
||||||
|
case 'youtube':
|
||||||
|
return faYoutube;
|
||||||
|
default:
|
||||||
|
return faGlobe;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="profile-top bg-primary p-2">
|
||||||
|
<img src={avatarUrl} alt={displayName} className="round-img my-1" />
|
||||||
|
<h1 className="large">{displayName}</h1>
|
||||||
|
<p className="lead">{getDescription(status, company)}</p>
|
||||||
|
<p>{location}</p>
|
||||||
|
<div className="icons my-1">
|
||||||
|
{Object.entries(links)
|
||||||
|
.sort()
|
||||||
|
.map(([icon, webAddress], i: number) => (
|
||||||
|
<a
|
||||||
|
href={webAddress}
|
||||||
|
key={i}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={renderSocialIcon(icon)} size="2x" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ProfileTop);
|
||||||
69
src/pages/Profile/index.tsx
Normal file
69
src/pages/Profile/index.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React, {FC} from 'react';
|
||||||
|
// Redux
|
||||||
|
import {compose} from '@reduxjs/toolkit';
|
||||||
|
import {firestoreConnect} from 'react-redux-firebase';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {RootState} from '../../store';
|
||||||
|
// Routing
|
||||||
|
import {Link, useParams} from 'react-router-dom';
|
||||||
|
import Routes from '../../constants/routes';
|
||||||
|
import NotFound from '../NotFound';
|
||||||
|
// Typing
|
||||||
|
import IDev from '../../models/Dev';
|
||||||
|
|
||||||
|
import ProfileTop from './Top';
|
||||||
|
import ProfileAbout from './About';
|
||||||
|
import ProfileExperience from './Experience';
|
||||||
|
import ProfileEducation from './Education';
|
||||||
|
import ProfileGithub from './Github';
|
||||||
|
import Collections from '../../constants/collections';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
dev: IDev;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dev personal profile as seen by other people.
|
||||||
|
*/
|
||||||
|
const ProfileComponent: FC<IProps> = ({dev}) => {
|
||||||
|
// display 404 page if dev is null
|
||||||
|
if (dev === null) {
|
||||||
|
return <NotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dev === undefined ? (
|
||||||
|
<div>Loading ... </div>
|
||||||
|
) : (
|
||||||
|
<section className="container">
|
||||||
|
<Link to={Routes.DEVELOPERS} className="btn">
|
||||||
|
Back to profiles
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="profile-grid my-1">
|
||||||
|
<ProfileTop {...dev} />
|
||||||
|
<ProfileAbout {...dev} />
|
||||||
|
<ProfileExperience experiences={dev.experiences} />
|
||||||
|
<ProfileEducation educations={dev.educations} />
|
||||||
|
<ProfileGithub repos={dev.repos} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container to fetch id params from thr URI and pass it to Profile page
|
||||||
|
*/
|
||||||
|
const Profile: FC = () => {
|
||||||
|
const {id} = useParams();
|
||||||
|
|
||||||
|
const Component = compose<FC>(
|
||||||
|
firestoreConnect(() => [`${Collections.USERS}/${id}`]),
|
||||||
|
connect(({firestore: {data}}: RootState) => ({
|
||||||
|
dev: data.users && data.users[id],
|
||||||
|
})),
|
||||||
|
)(ProfileComponent);
|
||||||
|
|
||||||
|
return <Component />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
1
src/static/img/education.svg
Normal file
1
src/static/img/education.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.4 KiB |
1
src/static/img/experience.svg
Normal file
1
src/static/img/experience.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
1
src/static/img/github.svg
Normal file
1
src/static/img/github.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 19 KiB |
Loading…
Reference in a new issue