diff --git a/workspaces/components/src/devto/README.md b/workspaces/components/src/devto/README.md deleted file mode 100644 index 483741f..0000000 --- a/workspaces/components/src/devto/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Dev.to profile card widget - -WIP for use on scottnath.com only - -## @todo - -- [ ] Fetch non-key data from api -- [ ] container queries and nested CSS -- [ ] move to separate repo - - [ ] profile-cards -- [ ] i18 || configure titles -- [ ] use `data-thing` for attributes? -- [ ] re-do `parts`, removing parts that are just for style-sharing -- [ ] interaction tests -- [ ] a11y testing -- [ ] create custom element manifest - - [ ] need [plugins](https://custom-elements-manifest.open-wc.org/blog/intro/#plugins) to do JSDoc correctly -- [x] separate out fetch and data handling from component -- [ ] alt-option that allows use of api-key -- [ ] test in plain HTML page -- [ ] test in Qwik -- [ ] test in Next.js - -## Inspriation - -- https://dev.to/asheeshh/devembed-embed-your-devto-profile-anywhere-using-widgets-linode-hacakathon-4659 -- https://dev.to/saurabhdaware/i-made-dev-to-widget-for-websites-blogs-40p2 \ No newline at end of file diff --git a/workspaces/components/src/devto/devto.shared-spec.js b/workspaces/components/src/devto/devto.shared-spec.js deleted file mode 100644 index a1e9506..0000000 --- a/workspaces/components/src/devto/devto.shared-spec.js +++ /dev/null @@ -1,30 +0,0 @@ - -export const smileySvg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAJUlEQVR42u3NQQEAAAQEsJNcdFLw2gqsMukcK4lEIpFIJBLJS7KG6yVo40DbTgAAAABJRU5ErkJggg==' - -export const snUserFixture = { - id: 1055555, - username: 'scottnath', - name: 'Scott Nath', - summary: "Front-end Architect", - joined_at: 'Mar 30, 2023', - // profile_image: 'https://res.cloudinary.com/practicaldev/image/fetch/s--8gi1l6OI--/c_fill,f_auto,fl_progressive,h_320,q_auto,w_320/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/1055555/8146c5bb-31d3-4023-a216-5cb5c00ecb3b.jpg', - profile_image: './multi-face-image.jpeg', - joined: '2023-03-30', - post_count: 8 -} - -export const snPostFixture = { - title: 'Sharing UI Tests Between Javascript Frameworks', - description: 'How to share testing-library UI tests between Javascript frameworks with the same or similar components and use them in Storybook and unit testing.', - url: 'https://dev.to/scottnath/sharing-ui-tests-between-javascript-frameworks-2l6n', - cover_image: 'https://res.cloudinary.com/practicaldev/image/fetch/s--NqWkGO2---/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z5070dozgnb32uwtm5dm.png', -} - -export const meowUserFixture = { - id: 1055555, - username: 'meowmeow', - name: 'Meow Meow', - summary: "Just a meow, with a meow's worries. Nothin but a meow.", - joined_at: 'Feb 29, 2020', - profile_image: smileySvg, -} diff --git a/workspaces/components/src/devto/devto.stories.js b/workspaces/components/src/devto/devto.stories.js deleted file mode 100644 index 0ecf3fa..0000000 --- a/workspaces/components/src/devto/devto.stories.js +++ /dev/null @@ -1,61 +0,0 @@ - -import './'; -import { snUserFixture, meowUserFixture } from './devto.shared-spec'; - -export default { - title: 'DevTo', - component: 'dev-user', - tags: ['autodocs'], -}; - -export const Username = { - args: { - username: 'scottnath', - }, -}; - -export const NewUser = { - args: { - username: 'soyecoder', - }, -} - -export const AltUserData = { - args: { - user: { - ...snUserFixture, - name: 'Someother Name', - summary: 'Different summary content than what is from the dev.to api', - joined_at: 'Jan 1, 1979', - post_count: 1, - }, - }, -} - -export const SkipFetch = { - args: { - user: meowUserFixture, - skipFetch: true, - }, -} - -export const SkipFetchJustUsername = { - args: { - user: { - username: 'scottnath' - }, - skipFetch: true, - }, -} - -export const SkipFetchFail = { - args: { - skipFetch: true, - }, -} - -export const UnknownUsername = { - args: { - username: 'NotAUserMeow', - }, -} diff --git a/workspaces/components/src/devto/get-devto-user.js b/workspaces/components/src/devto/get-devto-user.js deleted file mode 100644 index 1cd935c..0000000 --- a/workspaces/components/src/devto/get-devto-user.js +++ /dev/null @@ -1,76 +0,0 @@ - -/** - * Content about one post by dev.to (or Forem) user, sourced from the Forem API. - * @see https://developers.forem.com/api/v1#tag/articles/operation/getLatestArticles - * @typedef {Object} ForemPost - * @property {string} title - The title of the post - * @property {string} url - The URL of the post - * @property {string} cover_image - The URL of the post's full-size cover image - */ - -/** - * Content about a dev.to (or Forem) user, sourced from the Forem API and combined with post data. - * Only required properties from the api are defined. - * @see https://developers.forem.com/api/v0#tag/users/operation/getUser - * @typedef {Object} ForemUser - * - * @property {string} username - The username of the user - * @property {string} name - The name of the user - * @property {string} summary - The user's bio - * @property {string} joined_at - The date the user joined - * @property {string} profile_image - The URL of the user's profile image - * @property {number} post_count - The number of posts the user has published - */ - -/** - * @function Fetch a user's posts from the Forem API - * @param {string} username - * @returns {ForemPost[]} - An array of posts - */ -export const fetchUserPosts = async (username) => { - const articles = await fetch(`https://dev.to/api/articles/latest?per_page=1000&username=${username?.toLowerCase()}`); - const articlesJson = await articles.json(); - return articlesJson; -} - -/** - * @function Fetch a user's data from the Forem API - * @property {string} username - The username of the user - * @property {string} id - the id of the user - */ -export const fetchUser = async (username, id) => { - let response; - if (!username && id) { - response = await fetch(`https://dev.to/api/users/${id}`); - } else { - response = await fetch(`https://dev.to/api/users/by_username?url=${username?.toLowerCase()}`); - } - const userJson = await response.json(); - return userJson; -} - -/** - * Get a user's profile and posts - * @param {string} username - * @param {string} id - * @returns {Promise<{user: ForemUser, posts: ForemPost[]}>} - */ -export const getUserContent = async (username, id) => { - const user = await fetchUser(username, id); - if (user.error) { - return { - error: user.error, - } - } - if (!user.username) { - return { - error: 'no username found', - } - } - const posts = await fetchUserPosts(user.username); - user.post_count = posts.length; - return { - user, - posts, - } -} diff --git a/workspaces/components/src/devto/index.js b/workspaces/components/src/devto/index.js deleted file mode 100644 index 6bddd6b..0000000 --- a/workspaces/components/src/devto/index.js +++ /dev/null @@ -1,270 +0,0 @@ -import { LitElement, html, css, unsafeCSS } from 'lit'; -import {when} from 'lit/directives/when.js'; - -// import stylesDep from './style.css' -import styles from './styles.css?inline' - -/** - * Blank base64-encoded png - * @see https://png-pixel.com/ - */ -const blankPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mN8/x8AAuMB8DtXNJsAAAAASUVORK5CYII='; - -/** - * dev.to logo - * @see https://dev.to/brand - */ -const devLogoSvg = html``; - -/** - * Forem icon for a cake (used with "Joined" date) - * @see https://github.com/forem/forem/blob/main/app/assets/images/cake.svg?short_path=e3c7d41 - */ -const joinedSvg = html``; - -/** - * Forem icon for a post - * @see https://github.com/forem/forem/blob/main/app/assets/images/post.svg?short_path=b79fa43 - */ -const postSvg = html``; - -/** - * Content about one post by dev.to (or Forem) user, sourced from the Forem API. - * @see https://developers.forem.com/api/v1#tag/articles/operation/getLatestArticles - * @typedef {Object} ForemPost - * @property {string} title - The title of the post - * @property {string} url - The URL of the post - * @property {string} cover_image - The URL of the post's full-size cover image - */ - -/** - * Render a link to a post - * @param {ForemPost} post - Content about a post - */ -const postLink = (post) => html` - -${post.title}`; - -/** - * Content about a dev.to (or Forem) user, sourced from the Forem API and combined with post data. - * Only the properties used in this component are defined. - * @see https://developers.forem.com/api/v0#tag/users/operation/getUser - * @typedef {Object} ForemUser - * - * @property {string} username - The username of the user - * @property {string} name - The name of the user - * @property {string} summary - The user's bio - * @property {string} joined_at - The date the user joined - * @property {string} profile_image - The URL of the user's profile image - * @property {number} post_count - The number of posts the user has published - */ - - -/** - * Render a link to a user's profile - * @param {ForemUser} user - Content about a user - */ -const profileLink = (user) => html` -
- View Profile on dev.to - -`; - -/** - * Render a user's avatar - * @param {ForemUser} user - Content about a user - */ -const userAvatar = (user) => html``; - -/** - * Render a user's joined date - * @param {ForemUser} user - Content about a user - */ -const userJoined = (user) => this.user?.joined_at ? html`-${joinedSvg} -Joined on - - -
-` : ''; - -/** - * dev.to profile component - * @element dev-user - * @cssprop --devto-color - * @prop {string} username - The username of the user - * @prop {ForemUser} user - Content about a user - * @prop {ForemPost} latest_post - Content about a post - */ -export class DevToProfile extends LitElement { - static properties = { - user: { type: Object }, - username: { type: String }, - latest_post: { type: Object }, - skipFetch: { type: Boolean }, - }; - static styles = css` - ${unsafeCSS(styles)} - `; - - async firstUpdated() { - if (!this.username && this.user?.username) { - this.username = this.user.username; - } - if (this.skipFetch) { - if (!this.username) { - this._generateError( - 'A username is required to skip fetching data', - 'UI requires a name and username at minimum', - ) - return; - } - } else { - if (this.username) { - await this._generateUser(this.username); - } else { - await this._generateUser(null, this.user?.id); - } - } - await this._cleanUserData(); - } - - /** - * Format a date for machine-readability - * @param {string} dt - * @returns {string} - the machine-readable value of the date - */ - _formatDate(dt) { - const x = new Date(dt); - const year = x.getFullYear() - const month = String(x.getMonth() + 1).padStart(2, '0') - const day = String(x.getDate()).padStart(2, '0') - - return `${year}-${month}-${day}` - } - - async _fetchPosts(username) { - const articles = await fetch(`https://dev.to/api/articles/latest?per_page=1000&username=${username?.toLowerCase()}`); - const articlesJson = await articles.json(); - this.user.post_count = this.user.post_count || articlesJson.length; - if (articlesJson.length && !this.latest_post) { - this.latest_post = articlesJson[0]; - } - } - - async _fetchUserResponse(username, id) { - if (!username && id) { - return await fetch(`https://dev.to/api/users/${id}`); - } - return await fetch(`https://dev.to/api/users/by_username?url=${username?.toLowerCase()}`); - } - - async _generateError(msg, status) { - this.user = { - name: msg, - status, - } - } - - async _generateUser(username, id) { - const response = await this._fetchUserResponse(username, id); - const jsonResponse = await response.json(); - if (jsonResponse.error) { - this._generateError( - `User ${username || id} ${jsonResponse.error}`, - jsonResponse.error, - ) - return; - } - this.user = { - ...jsonResponse, - ...this.user, - } - if (typeof this.user.post_count !== 'number' || (this.user.post_count > 0 && !this.latest_post)) { - await this._fetchPosts(this.user.username); - } - } - - /** - * Clean up data to conform to the HTML-expected content model - */ - async _cleanUserData() { - this.user.profile_image = this.user.profile_image || blankPng; - this.user.name = this.user.name || `@${this.user.username}`; - if (this.user.joined_at) { - this.user.joined = this.user.joined || this._formatDate(this.user.joined_at); - } - if (this.user.post_count && Number(this.user.post_count) !== NaN) { - this.user.post_count = Number(this.user.post_count); - } else { - delete this.user?.post_count; - } - this.latest_post.cover_image = this.latest_post.cover_image || blankPng; - } - - render() { - if (this.user?.status) { - return html` -- ${this.user?.name} -
-${this.user?.summary}
`)} - ${when(this.user?.joined_at, () => html`- ${joinedSvg} - Joined on - - -
`)} - ${when(this.user?.post_count > 0, () => html` -- ${postSvg} - ${this.user?.post_count} posts published -
- `)} - ${when(this.latest_post, () => html` -${summary}
` : ''; - -/** - * @function Render a user's joined date - * @param {string} joined_at - date the user joined - */ -export const userJoined = (joined_at) => joined_at ? html`-${joinedSvg} -Joined on - - -
-` : ''; - -/** - * @function Render a user's post count - * @param {number} post_count - number of posts the user has published - */ -export const userPostCount = (post_count) => post_count !== undefined ? html`- ${postSvg} - ${post_count} posts published -
-` : ''; - -/** - * dev.to profile card component - * @element devto-profile-card - * @prop {ForemUser} user - Content about a user - * @prop {ForemPost} [latest_post] - Content about a post - */ -export class DevToProfileCard extends LitElement { - static properties = { - user: { type: Object }, - latest_post: { type: Object }, - }; - static styles = css` - ${unsafeCSS(styles)} - `; - - constructor() { - super(); - this.error = null; - } - - connectedCallback() { - super.connectedCallback(); - this._cleanUserData(); - } - - /** - * Clean up data to conform to the HTML-expected content model - */ - _cleanUserData() { - if (!this.user) { - this.error = 'No user data provided'; - return; - } - if (!this.user?.username) { - this.error = 'Username (user.username) is required'; - return; - } - this.user.profile_image = this.user.profile_image || blankPng; - this.user.name = this.user.name || `@${this.user.username}`; - if (this.user.post_count && Number(this.user.post_count) !== NaN) { - this.user.post_count = Number(this.user.post_count); - } else { - delete this.user?.post_count; - } - if (this.latest_post) { - this.latest_post.cover_image = this.latest_post.cover_image || blankPng; - } - } - - render() { - if (this.error) { - return html` -- ${this.error} -
-