-
-
Notifications
You must be signed in to change notification settings - Fork 94
Feat/talking about #682
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
Merged
Merged
Feat/talking about #682
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| --- | ||
| title: Articles | ||
| description: Blog posts, tutorials and write-ups the community published about Zard UI, gathered in one place. | ||
| --- | ||
|
|
||
| # Articles | ||
|
|
||
| Blog posts, tutorials and write-ups the community published about Zard UI, gathered in one place. | ||
|
|
||
| Made by the community | ||
|
|
||
| Every article below was written by someone outside the core team. Each card opens the original post on the platform where it was published, so the author keeps the traffic and the credit. | ||
|
|
||
| ## English | ||
|
|
||
| - [Building fast in Angular with Zard UI, Tailwind CSS and Signals](https://dev.to/hassantayyab/building-fast-in-angular-with-zard-ui-tailwind-css-and-signals-cj5) | ||
| - [ZardUI Beta: Bringing shadcn/ui's Philosophy to Angular - Where You Own Every Line of Code](https://dev.to/samuelrizzondev/zardui-beta-bringing-shadcnuis-philosophy-to-angular-where-you-own-every-line-of-code-2a79) | ||
|
|
||
| ## Contribute | ||
|
|
||
| Wrote an article about Zard UI? | ||
|
|
||
| Send the link to [hello@luizgomes.dev](mailto:hello@luizgomes.dev?subject=Zard%20UI%20article%20submission) and we will add it to this page. Submissions go through a review before being published, so it may take a while until your article shows up here. Content in any language is welcome. |
90 changes: 90 additions & 0 deletions
90
apps/web/src/app/domain/pages/featured/articles/articles.page.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| <z-content | ||
| [navigationConfig]="navigationConfig()" | ||
| [activeAnchor]="activeAnchor" | ||
| scrollSpy | ||
| (scrollSpyChange)="activeAnchor = $event" | ||
| > | ||
| <z-doc-heading | ||
| title="Articles" | ||
| description="Blog posts, tutorials and write-ups the community published about Zard UI, gathered in one place." | ||
| scrollSpyItem="overview" | ||
| id="overview" | ||
| ></z-doc-heading> | ||
|
|
||
| <z-alert | ||
| zTitle="Made by the community" | ||
| zDescription="Every article below was written by someone outside the core team. Each card opens the original post on the platform where it was published, so the author keeps the traffic and the credit." | ||
| /> | ||
|
|
||
| @for (group of groups(); track group.id) { | ||
| <section class="flex flex-col gap-6 sm:gap-8" [scrollSpyItem]="group.id" [id]="group.id"> | ||
| <h2 class="font-heading mt-12 scroll-m-28 text-2xl font-medium tracking-tight first:mt-0 lg:mt-20"> | ||
| {{ group.label }} | ||
| </h2> | ||
|
|
||
| <div class="grid grid-cols-1 gap-6 sm:grid-cols-2"> | ||
| @for (article of group.articles; track article.id) { | ||
| <a | ||
| [href]="article.url" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| class="group focus-visible:ring-ring focus-visible:ring-offset-background flex flex-col gap-3 rounded-lg outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2" | ||
| > | ||
| <div class="bg-muted relative aspect-video overflow-hidden rounded-lg border"> | ||
| @if (article.coverFailed) { | ||
| <div class="flex h-full w-full flex-col items-center justify-center gap-3 px-6"> | ||
| <img src="/images/zard.svg" alt="" aria-hidden="true" class="h-10 w-auto opacity-40 dark:invert" /> | ||
| <p class="text-muted-foreground line-clamp-2 text-center text-sm font-medium">{{ article.title }}</p> | ||
| </div> | ||
| } @else { | ||
| <img | ||
| [src]="article.cover" | ||
| [alt]="'Cover image: ' + article.title" | ||
| loading="lazy" | ||
| decoding="async" | ||
| class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" | ||
| (error)="onCoverError(article.id)" | ||
| /> | ||
| } | ||
|
|
||
| <div | ||
| class="absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100" | ||
| > | ||
| <span | ||
| class="bg-foreground text-background flex h-14 w-14 items-center justify-center rounded-full shadow-lg transition-transform duration-300 group-hover:scale-110" | ||
| > | ||
| <ng-icon name="lucideArrowUpRight" class="size-6!" aria-hidden="true" /> | ||
| </span> | ||
| </div> | ||
|
|
||
| <z-badge zType="secondary" class="absolute top-3 right-3">{{ article.source }}</z-badge> | ||
| </div> | ||
|
|
||
| <div class="flex flex-col gap-1"> | ||
| <h3 class="line-clamp-2 text-sm leading-snug font-medium text-balance">{{ article.title }}</h3> | ||
| <p class="text-muted-foreground text-xs"> | ||
| {{ article.author }} · {{ article.publishedAt | date: 'MMM d, y' : 'UTC' }} · | ||
| {{ article.readingTime }} min read | ||
| </p> | ||
| </div> | ||
| </a> | ||
| } | ||
| </div> | ||
| </section> | ||
| } | ||
|
|
||
| <section class="flex flex-col gap-6 sm:gap-8" scrollSpyItem="contribute" id="contribute"> | ||
| <h2 class="font-heading mt-12 scroll-m-28 text-2xl font-medium tracking-tight first:mt-0 lg:mt-20">Contribute</h2> | ||
|
|
||
| <z-alert zTitle="Wrote an article about Zard UI?" [zDescription]="contributeDescription" /> | ||
|
|
||
| <ng-template #contributeDescription> | ||
| <p> | ||
| Send the link to | ||
| <a href="mailto:hello@luizgomes.dev?subject=Zard%20UI%20article%20submission">hello@luizgomes.dev</a> | ||
| and we will add it to this page. Submissions go through a review before being published, so it may take a while | ||
| until your article shows up here. Content in any language is welcome. | ||
| </p> | ||
| </ng-template> | ||
| </section> | ||
| </z-content> | ||
111 changes: 111 additions & 0 deletions
111
apps/web/src/app/domain/pages/featured/articles/articles.page.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
| import { By } from '@angular/platform-browser'; | ||
| import { provideRouter } from '@angular/router'; | ||
|
|
||
| import { ScrollSpyItemDirective } from '@doc/domain/directives/scroll-spy-item.directive'; | ||
|
|
||
| import { ArticlesPage } from './articles.page'; | ||
| import { FEATURED_ARTICLES } from '../data/featured-articles'; | ||
|
|
||
| describe('ArticlesPage', () => { | ||
| let component: ArticlesPage; | ||
| let fixture: ComponentFixture<ArticlesPage>; | ||
|
|
||
| const cards = () => fixture.debugElement.queryAll(By.css('a[target="_blank"][rel="noopener noreferrer"]')); | ||
|
|
||
| const imageSources = () => | ||
| fixture.debugElement.queryAll(By.css('img')).map(el => (el.nativeElement as HTMLImageElement).getAttribute('src')); | ||
|
|
||
| beforeEach(async () => { | ||
| await TestBed.configureTestingModule({ | ||
| imports: [ArticlesPage], | ||
| providers: [provideRouter([])], | ||
| }).compileComponents(); | ||
|
|
||
| fixture = TestBed.createComponent(ArticlesPage); | ||
| component = fixture.componentInstance; | ||
| fixture.detectChanges(); | ||
| }); | ||
|
|
||
| it('creates', () => { | ||
| expect(component).toBeTruthy(); | ||
| }); | ||
|
|
||
| describe('article cards', () => { | ||
| it('renders one card per article', () => { | ||
| expect(cards()).toHaveLength(FEATURED_ARTICLES.length); | ||
| }); | ||
|
|
||
| it('links every card to the original post in a new tab', () => { | ||
| const hrefs = cards().map(card => (card.nativeElement as HTMLAnchorElement).getAttribute('href')); | ||
|
|
||
| for (const article of FEATURED_ARTICLES) { | ||
| expect(hrefs).toContain(article.url); | ||
| } | ||
| }); | ||
|
|
||
| it('shows the author, the publication date and the reading time', () => { | ||
| const text = (fixture.nativeElement as HTMLElement).textContent ?? ''; | ||
|
|
||
| expect(text).toContain('Samuel Rizzon'); | ||
| expect(text).toContain('Aug 19, 2025'); | ||
| expect(text).toContain('4 min read'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('navigation', () => { | ||
| // Bound as `[scrollSpyItem]`/`[id]` on the language sections, so the directive — not the | ||
| // attribute selector — is what finds every spied section. | ||
| const sectionIds = () => | ||
| fixture.debugElement | ||
| .queryAll(By.directive(ScrollSpyItemDirective)) | ||
| .map(el => (el.nativeElement as HTMLElement).id); | ||
|
|
||
| it('has a navigation entry for every section in the template', () => { | ||
| const configured = component['navigationConfig']().items.map(item => item.id); | ||
|
|
||
| for (const id of sectionIds()) { | ||
| expect(configured).toContain(id); | ||
| } | ||
| }); | ||
|
|
||
| it('has a section in the template for every navigation entry', () => { | ||
| const rendered = sectionIds(); | ||
|
|
||
| for (const item of component['navigationConfig']().items) { | ||
| expect(rendered).toContain(item.id); | ||
| } | ||
| }); | ||
|
|
||
| it('skips languages that have no article yet', () => { | ||
| const configured = component['navigationConfig']().items.map(item => item.id); | ||
|
|
||
| expect(configured).not.toContain('portuguese'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('contribute section', () => { | ||
| it('points to the submission address', () => { | ||
| const link = fixture.debugElement.query(By.css('section#contribute a[href^="mailto:"]')); | ||
|
|
||
| expect((link.nativeElement as HTMLAnchorElement).getAttribute('href')).toBe( | ||
| 'mailto:hello@luizgomes.dev?subject=Zard%20UI%20article%20submission', | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('cover fallback', () => { | ||
| it('drops the remote image when the cover fails to load', () => { | ||
| const article = FEATURED_ARTICLES[0]; | ||
|
|
||
| expect(imageSources()).toContain(article.cover); | ||
| expect(imageSources()).not.toContain('/images/zard.svg'); | ||
|
|
||
| component['onCoverError'](article.id); | ||
| fixture.detectChanges(); | ||
|
|
||
| expect(imageSources()).not.toContain(article.cover); | ||
| expect(imageSources()).toContain('/images/zard.svg'); | ||
| }); | ||
| }); | ||
| }); |
104 changes: 104 additions & 0 deletions
104
apps/web/src/app/domain/pages/featured/articles/articles.page.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { DatePipe } from '@angular/common'; | ||
| import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; | ||
|
|
||
| import { NgIcon, provideIcons } from '@ng-icons/core'; | ||
| import { lucideArrowUpRight } from '@ng-icons/lucide'; | ||
|
|
||
| import { DocContentComponent } from '@doc/domain/components/doc-content/doc-content.component'; | ||
| import { DocHeadingComponent } from '@doc/domain/components/doc-heading/doc-heading.component'; | ||
| import { NavigationConfig } from '@doc/domain/components/dynamic-anchor/dynamic-anchor.component'; | ||
| import { ScrollSpyItemDirective } from '@doc/domain/directives/scroll-spy-item.directive'; | ||
| import { ScrollSpyDirective } from '@doc/domain/directives/scroll-spy.directive'; | ||
| import { SeoService } from '@doc/shared/services/seo.service'; | ||
|
|
||
| import { ZardAlertComponent } from '@zard/components/alert/alert.component'; | ||
| import { ZardBadgeComponent } from '@zard/components/badge/badge.component'; | ||
|
|
||
| import { FEATURED_ARTICLES, type FeaturedArticle, type FeaturedArticleLanguage } from '../data/featured-articles'; | ||
|
|
||
| interface ArticleCard extends FeaturedArticle { | ||
| /** Whether the remote cover failed to load and the card must render the placeholder instead. */ | ||
| coverFailed: boolean; | ||
| } | ||
|
|
||
| interface ArticleGroup { | ||
| id: string; | ||
| label: string; | ||
| articles: ArticleCard[]; | ||
| } | ||
|
|
||
| const LANGUAGE_GROUPS: ReadonlyArray<{ id: string; label: string; language: FeaturedArticleLanguage }> = [ | ||
| { id: 'portuguese', label: 'Portuguese 🇧🇷', language: 'pt-BR' }, | ||
| { id: 'english', label: 'English', language: 'en' }, | ||
| ]; | ||
|
|
||
| @Component({ | ||
| selector: 'z-articles', | ||
| standalone: true, | ||
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| templateUrl: './articles.page.html', | ||
| imports: [ | ||
| DatePipe, | ||
| DocContentComponent, | ||
| DocHeadingComponent, | ||
| NgIcon, | ||
| ZardAlertComponent, | ||
| ZardBadgeComponent, | ||
| ScrollSpyDirective, | ||
| ScrollSpyItemDirective, | ||
| ], | ||
| viewProviders: [provideIcons({ lucideArrowUpRight })], | ||
| }) | ||
|
Luizgomess marked this conversation as resolved.
|
||
| export class ArticlesPage implements OnInit { | ||
| private readonly seoService = inject(SeoService); | ||
| activeAnchor?: string; | ||
|
|
||
| /** | ||
| * Seeded synchronously, unlike the YouTube page: the anchor list is derived from this data, and | ||
| * filling it in after the first render leaves the server-rendered anchors and the client out of | ||
| * sync — the hydrated `@for` ends up moving the language anchor past `Contribute`. The list still | ||
| * stays out of the initial bundle because the whole route is lazy-loaded. | ||
| */ | ||
| private readonly articles = signal<readonly FeaturedArticle[]>(FEATURED_ARTICLES); | ||
| /** IDs whose cover could not be fetched (CDN down, hotlink blocked, article unpublished). */ | ||
| private readonly degradedCovers = signal<ReadonlySet<string>>(new Set<string>()); | ||
|
|
||
| protected readonly groups = computed<ArticleGroup[]>(() => { | ||
| const articles = this.articles(); | ||
| const degraded = this.degradedCovers(); | ||
|
|
||
| return LANGUAGE_GROUPS.map(group => ({ | ||
| id: group.id, | ||
| label: group.label, | ||
| articles: articles | ||
| .filter(article => article.language === group.language) | ||
| // Copy before sorting: the source array is readonly and must never be mutated. | ||
| .slice() | ||
| .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)) | ||
| .map(article => ({ ...article, coverFailed: degraded.has(article.id) })), | ||
| })).filter(group => group.articles.length > 0); | ||
| }); | ||
|
|
||
| /** Derived from the rendered groups so an empty language never leaves a dangling anchor. */ | ||
| protected readonly navigationConfig = computed<NavigationConfig>(() => ({ | ||
| items: [ | ||
| { id: 'overview', label: 'Overview', type: 'core' }, | ||
| ...this.groups().map(group => ({ id: group.id, label: group.label, type: 'custom' as const })), | ||
| { id: 'contribute', label: 'Contribute', type: 'custom' }, | ||
| ], | ||
| })); | ||
|
|
||
| ngOnInit(): void { | ||
| this.seoService.setDocsSeo( | ||
| 'Articles', | ||
| 'Blog posts, tutorials and write-ups the community published about Zard UI, gathered in one place.', | ||
| '/docs/featured/articles', | ||
| 'og-featured-articles.jpg', | ||
| ); | ||
| } | ||
|
|
||
| /** Covers are hotlinked from the publisher's CDN, so a broken URL must degrade to the placeholder. */ | ||
| protected onCoverError(id: string): void { | ||
| this.degradedCovers.update(current => (current.has(id) ? current : new Set(current).add(id))); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.