Skip to content

Commit

Permalink
refactor: decompose repository card into interface and display
Browse files Browse the repository at this point in the history
  • Loading branch information
Tsukina-7mochi committed Jun 11, 2024
1 parent 526d58a commit 7064c85
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 120 deletions.
7 changes: 4 additions & 3 deletions import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
"esbuild": "https://deno.land/x/[email protected]/mod.js",
"esbuild-plugin-cache": "https://deno.land/x/[email protected]/mod.ts",
"github-colors": "https://raw.githubusercontent.com/ozh/github-colors/master/colors.json",
"lit": "npm:[email protected]",
"lit/decorators.js": "npm:[email protected]/decorators.js",
"lit/task": "npm:@lit/[email protected]",
"lit": "npm:/[email protected]",
"lit/": "npm:/[email protected]/",
"lit/decorators.js": "npm:/[email protected]/decorators.js",
"lit/task": "npm:/@lit/[email protected]",
"std/": "https://deno.land/[email protected]/"
}
}
1 change: 1 addition & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import './cardBase.ts';
import './repositoryCard.ts';
import './repositoryCardDisplay.ts';
139 changes: 22 additions & 117 deletions src/repositoryCard.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,14 @@
import { css, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { Task } from 'lit/task';
import languageColors from 'github-colors' with { type: 'json' };
import { ifDefined } from 'lit/directives/if-defined.js';

import { errorElement } from './errorElement.ts';
import { getRepository } from './lib/ghApi.ts';
import { forkIcon, licenseIcon, starIcon, tagIcon } from './icons.ts';
import { pendingElement } from './pendingElement.ts';

const languageColorElement = function (language: string): TemplateResult {
const colorCode = languageColors[language]?.color ?? '#808080';
const color = unsafeCSS(colorCode);

return html`<span style="display: inline-block; width: 0.9em; height: 0.9em; vertical-align: middle; border-radius: 50%; background-color: ${color};"></span>`;
};

@customElement('gh-repo-card')
export class RepositoryCard extends LitElement {
static styles = css`
:host {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
--c-fg: var(--gh-card-color-fg, #404040);
--c-fg-2: var(--gh-card-color-fg-2, #808080);
--c-link: var(--gh-card-color-link, #646cff);
}
gh-card::part(wrapper) {
padding: 0;
}
svg.icon {
width: 1em;
height: 1em;
vertical-align: middle;
fill: var(--c-fg);
}
a {
color: #646cff;
}
a#wrapper {
display: flex;
flex-direction: column;
gap: 0.35em;
color: inherit;
text-decoration: none;
color: var(--c-fg);
line-height: 1.5;
text-indent: 0;
}
h2 {
margin: 0;
font-weight: bold;
font-size: 1.2em;
color: var(--c-link);
}
header {
display: flex;
align-items: center;
gap: 1em;
}
header+div {
display: flex;
gap: 1em;
}
#avatar {
height: 3.5em;
border-radius: 4px;
}
#description {
margin: 0;
}
#source {
font-size: 0.9em;
color: var(--c-fg-2);
}
#topics > span {
margin-right: 0.5em;
}
`;

@property()
accessor name = '';

Expand Down Expand Up @@ -135,41 +53,28 @@ export class RepositoryCard extends LitElement {
return errorElement;
}

const avatar = html`<img id="avatar" src="${repo.owner.avatar_url}" alt="${repo.owner.login}">`;
const source = repo.source
? html`<div id="source">fork from <a href="${repo.source.html_url}">${repo.source.full_name}</a></div>`
: null;
const description = repo.description ? html`<p id="description">${repo.description}</p>` : null;
const languageColor = repo.language ? languageColorElement(repo.language) : null;
const language = html`<div id="language">${languageColor} ${repo.language}</div>`;
const stars = html`<div id="stars">${starIcon} ${repo.stargazers_count}</div>`;
const forks = html`<div id="forks">${forkIcon} ${repo.forks_count}</div>`;
const license = repo.license ? html`<div id="license">${licenseIcon} ${repo.license.name}</div>` : null;
const topicSpans = (repo.topics ?? []).map((v) => html`<span>${v}</span>`);
const topics = topicSpans.length > 0 ? html`<div id="topics">${tagIcon} ${topicSpans}</div>` : null;

// TODO: replace emojis in description
const avatarUrl = this.noAvatar ? null : repo.owner.avatar_url;
const description = this.noDescription ? null : repo.description;
const stars = this.noStars ? null : repo.stargazers_count;
const forks = this.noForks ? null : repo.forks_count;
const license = this.noLicense ? null : repo.license?.name;
const language = this.noLanguage ? null : repo.language;
const topics = this.noTopics ? [] : (repo.topics ?? []);

return html`
<gh-card-base intractable>
<a id="wrapper" href="${repo.html_url}">
<header>
${this.noAvatar ? null : avatar}
<div>
<h2>${repo.name}</h2>
${source}
${this.noDescription ? null : description}
</div>
</header>
<div>
${this.noLanguage ? null : language}
${this.noStars ? null : stars}
${this.noForks ? null : forks}
${this.noLicense ? null : license}
</div>
${this.noTopics ? null : topics}
</a>
</gh-card-base>
<gh-repo-card-display
name="${repo.name}"
url="${repo.html_url}"
owner="${repo.owner.login}"
avatar-url="${ifDefined(avatarUrl)}"
fork-source="${ifDefined(repo.source)}"
description="${ifDefined(description)}"
stars="${ifDefined(stars)}"
forks="${ifDefined(forks)}"
license="${ifDefined(license)}"
language="${ifDefined(language)}"
topics="${topics.join(' ')}"
></gh-repo-card-display>
`;
},
});
Expand Down
184 changes: 184 additions & 0 deletions src/repositoryCardDisplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { css, html, LitElement, nothing, TemplateResult, unsafeCSS } from 'lit';
import { map } from 'lit/directives/map.js';
import { customElement, property } from 'lit/decorators.js';
import languageColors from 'github-colors' with { type: 'json' };

import { forkIcon, licenseIcon, starIcon, tagIcon } from './icons.ts';

const arrayEquals = function (oldVar: string[], newVar: string[]) {
if (!(Array.isArray(oldVar) && Array.isArray(newVar))) return false;
if (oldVar.length !== newVar.length) return false;

for (let i = 0; i < oldVar.length; i++) {
if (oldVar[i] !== newVar[i]) return false;
}
return true;
};

const languageColorElement = function (language: string): TemplateResult {
const colorCode = languageColors[language]?.color ?? '#808080';
const color = unsafeCSS(colorCode);

return html`<span style="display: inline-block; width: 0.9em; height: 0.9em; vertical-align: middle; border-radius: 50%; background-color: ${color};"></span>`;
};

@customElement('gh-repo-card-display')
export class RepositoryCardDisplay extends LitElement {
static styles = css`
:host {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
--c-fg: var(--gh-card-color-fg, #404040);
--c-fg-2: var(--gh-card-color-fg-2, #808080);
--c-link: var(--gh-card-color-link, #646cff);
}
gh-card::part(wrapper) {
padding: 0;
}
svg.icon {
width: 1em;
height: 1em;
vertical-align: middle;
fill: var(--c-fg);
}
a {
color: #646cff;
}
a#wrapper {
display: flex;
flex-direction: column;
gap: 0.35em;
color: inherit;
text-decoration: none;
color: var(--c-fg);
line-height: 1.5;
text-indent: 0;
}
h2 {
margin: 0;
font-weight: bold;
font-size: 1.2em;
color: var(--c-link);
}
header {
display: flex;
align-items: center;
gap: 1em;
}
header+div {
display: flex;
gap: 1em;
}
#avatar {
height: 3.5em;
border-radius: 4px;
}
#description {
margin: 0;
}
#source {
font-size: 0.9em;
color: var(--c-fg-2);
}
#topics > span {
margin-right: 0.5em;
}
`;

@property({ type: String })
accessor name: string = '';

@property({ type: String })
accessor url: string = '';

@property({ type: String })
accessor owner: string = '';

@property({ type: String, attribute: 'avatar-url' })
accessor avatarUrl: string | null = null;

@property({ type: Object, attribute: 'fork-source' })
accessor forkSource: { html_url: string; full_name: string } | null = null;

@property({ type: String })
accessor description: string | null = null;

@property({ type: Number })
accessor stars: number | null = null;

@property({ type: Number })
accessor forks: number | null = null;

@property({ type: String })
accessor license: string | null = null;

@property({ type: String })
accessor language: string | null = null;

@property({
converter(value) {
if (typeof value !== 'string') {
return null;
}
return value.trim().split(' ').filter((v) => v.length > 0);
},
hasChanged: arrayEquals,
})
accessor topics: string[] = [];

render() {
const avatar = this.avatarUrl !== null
? html`<img id="avatar" src="${this.avatarUrl}" alt="${this.owner}">`
: nothing;
const forkSource = this.forkSource !== null
? html`<div id="source">fork from <a href="${this.forkSource.html_url}">${this.forkSource.full_name}</a></div>`
: nothing;
const description = this.description !== null ? html`<p id="description">${this.description}</p>` : nothing;
const language = this.language !== null
? html`<div id="language">${languageColorElement(this.language)} ${this.language}</div>`
: nothing;
const stars = this.stars !== null ? html`<div id="stars">${starIcon} ${this.stars}</div>` : nothing;
const forks = this.forks !== null ? html`<div id="forks">${forkIcon} ${this.forks}</div>` : nothing;
const license = this.license !== null ? html`<div id="license">${licenseIcon} ${this.license}</div>` : nothing;
const topicSpans = html`${map(this.topics, (topic) => html`<span>${topic}</span>`)}`;
const topicIcon = this.topics.length > 0 ? tagIcon : nothing;

return html`
<gh-card-base intractable>
<a id="wrapper" href="${this.url}">
<header>
${avatar}
<div>
<h2>${this.name}</h2>
${forkSource}
${description}
</div>
</header>
<div>
${language}
${stars}
${forks}
${license}
</div>
<div id="topics">
${topicIcon}
${topicSpans}
</div>
</a>
</gh-card-base>
`;
}
}

0 comments on commit 7064c85

Please sign in to comment.