From 494c2d862041987463f71e214200f303ba32a18b Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Tue, 19 Aug 2025 14:12:55 +0300 Subject: [PATCH 1/2] feat(overview-social-share): added socials share service --- .../share-and-download.component.html | 28 +++- .../share-and-download.component.ts | 74 +++------- .../overview-toolbar.component.html | 6 +- .../overview-toolbar.component.ts | 138 +++++++++++++----- .../project/overview/constants/index.ts | 1 - .../constants/social-actions.constants.ts | 18 --- .../features/project/overview/models/index.ts | 1 + .../models/socials-share-action-item.model.ts | 5 + .../overview/project-overview.component.html | 2 + src/app/shared/models/index.ts | 1 + src/app/shared/models/social-share.model.ts | 13 ++ src/app/shared/services/index.ts | 1 + .../shared/services/social-share.service.ts | 64 ++++++++ 13 files changed, 236 insertions(+), 116 deletions(-) delete mode 100644 src/app/features/project/overview/constants/index.ts delete mode 100644 src/app/features/project/overview/constants/social-actions.constants.ts create mode 100644 src/app/features/project/overview/models/socials-share-action-item.model.ts create mode 100644 src/app/shared/models/social-share.model.ts create mode 100644 src/app/shared/services/social-share.service.ts diff --git a/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.html b/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.html index 1480da297..1e4a38960 100644 --- a/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.html +++ b/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.html @@ -22,16 +22,36 @@
- - - -
diff --git a/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.ts b/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.ts index 07d1c0a4c..d2c392944 100644 --- a/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.ts +++ b/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.ts @@ -6,13 +6,13 @@ import { ButtonDirective } from 'primeng/button'; import { Card } from 'primeng/card'; import { Skeleton } from 'primeng/skeleton'; -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; import { IconComponent } from '@shared/components'; - -import { environment } from 'src/environments/environment'; +import { ShareableContent } from '@shared/models'; +import { SocialShareService } from '@shared/services'; @Component({ selector: 'osf-preprint-share-and-download', @@ -24,6 +24,8 @@ import { environment } from 'src/environments/environment'; export class ShareAndDownloadComponent { preprintProvider = input.required(); + private readonly socialShareService = inject(SocialShareService); + preprint = select(PreprintSelectors.getPreprint); isPreprintLoading = select(PreprintSelectors.isPreprintLoading); @@ -40,65 +42,33 @@ export class ShareAndDownloadComponent { if (!preprint) return '#'; - return `${environment.webUrl}/download/${this.preprint()?.id}`; + return this.socialShareService.createDownloadUrl(preprint.id); }); - private preprintDetailsFullUrl = computed(() => { + private shareableContent = computed((): ShareableContent | null => { const preprint = this.preprint(); const preprintProvider = this.preprintProvider(); - if (!preprint || !preprintProvider) return ''; + if (!preprint || !preprintProvider) return null; - return `${environment.webUrl}/preprints/${preprintProvider.id}/${preprint.id}`; + return { + id: preprint.id, + title: preprint.title, + description: preprint.description, + url: this.socialShareService.createPreprintUrl(preprint.id, preprintProvider.id), + }; }); - emailShareLink = computed(() => { - const preprint = this.preprint(); - const preprintProvider = this.preprintProvider(); + shareLinks = computed(() => { + const content = this.shareableContent(); - if (!preprint || !preprintProvider) return; + if (!content) return null; - const subject = encodeURIComponent(preprint.title); - const body = encodeURIComponent(this.preprintDetailsFullUrl()); - - return `mailto:?subject=${subject}&body=${body}`; + return this.socialShareService.generateAllSharingLinks(content); }); - twitterShareLink = computed(() => { - const preprint = this.preprint(); - const preprintProvider = this.preprintProvider(); - - if (!preprint || !preprintProvider) return ''; - - const url = encodeURIComponent(this.preprintDetailsFullUrl()); - const text = encodeURIComponent(preprint.title); - - return `https://twitter.com/intent/tweet?url=${url}&text=${text}`; - }); - - facebookShareLink = computed(() => { - const preprint = this.preprint(); - const preprintProvider = this.preprintProvider(); - - if (!preprint || !preprintProvider) return ''; - - const href = encodeURIComponent(this.preprintDetailsFullUrl()); - - const facebookAppId = preprintProvider.facebookAppId || environment.facebookAppId; - return `https://www.facebook.com/dialog/share?app_id=${facebookAppId}&display=popup&href=${href}`; - }); - - linkedInShareLink = computed(() => { - const preprint = this.preprint(); - const preprintProvider = this.preprintProvider(); - - if (!preprint || !preprintProvider) return ''; - - const url = encodeURIComponent(this.preprintDetailsFullUrl()); - const title = encodeURIComponent(preprint.title); - const summary = encodeURIComponent(preprint.description || preprint.title); - const source = encodeURIComponent('OSF'); - - return `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${title}&summary=${summary}&source=${source}`; - }); + emailShareLink = computed(() => this.shareLinks()?.email || ''); + twitterShareLink = computed(() => this.shareLinks()?.twitter || ''); + facebookShareLink = computed(() => this.shareLinks()?.facebook || ''); + linkedInShareLink = computed(() => this.shareLinks()?.linkedIn || ''); } diff --git a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html index 5668ddd7f..8f25ca2a1 100644 --- a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html +++ b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html @@ -91,11 +91,11 @@ [pTooltip]="'project.overview.tooltips.share' | translate" tooltipPosition="bottom" > - {{ socialsActionItems.length }} + {{ socialsActionItems().length }} - + - + diff --git a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts index 9b2e0b04c..df52ac8a2 100644 --- a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts +++ b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts @@ -11,16 +11,17 @@ import { Tooltip } from 'primeng/tooltip'; import { timer } from 'rxjs'; import { NgClass } from '@angular/common'; -import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, DestroyRef, effect, inject, input, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { DuplicateDialogComponent, TogglePublicityDialogComponent } from '@osf/features/project/overview/components'; +import { SocialsShareActionItem } from '@osf/features/project/overview/models'; import { IconComponent } from '@osf/shared/components'; -import { ToastService } from '@osf/shared/services'; +import { SocialShareService, ToastService } from '@osf/shared/services'; import { ResourceType } from '@shared/enums'; -import { ToolbarResource } from '@shared/models'; +import { ShareableContent, ToolbarResource } from '@shared/models'; import { FileSizePipe } from '@shared/pipes'; import { AddResourceToBookmarks, @@ -30,7 +31,6 @@ import { RemoveResourceFromBookmarks, } from '@shared/stores'; -import { SOCIAL_ACTION_ITEMS } from '../../constants'; import { ForkDialogComponent } from '../fork-dialog/fork-dialog.component'; @Component({ @@ -56,20 +56,27 @@ export class OverviewToolbarComponent { private dialogService = inject(DialogService); private translateService = inject(TranslateService); private toastService = inject(ToastService); + private socialShareService = inject(SocialShareService); protected destroyRef = inject(DestroyRef); private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); - isCollectionsRoute = input(false); protected isPublic = signal(false); protected isBookmarked = signal(false); + isCollectionsRoute = input(false); isAdmin = input.required(); currentResource = input.required(); + projectTitle = input(''); + projectDescription = input(''); showViewOnlyLinks = input(true); protected isBookmarksLoading = select(MyResourcesSelectors.getBookmarksLoading); protected isBookmarksSubmitting = select(BookmarksSelectors.getBookmarksCollectionIdSubmitting); protected bookmarksCollectionId = select(BookmarksSelectors.getBookmarksCollectionId); protected bookmarkedProjects = select(MyResourcesSelectors.getBookmarks); - protected readonly socialsActionItems = SOCIAL_ACTION_ITEMS; + protected socialsActionItems = computed(() => { + const shareableContent = this.createShareableContent(); + return shareableContent ? this.buildSocialActionItems(shareableContent) : []; + }); + protected readonly forkActionItems = [ { label: 'project.overview.actions.forkProject', @@ -120,38 +127,6 @@ export class OverviewToolbarComponent { }); } - private handleForkResource(): void { - const resource = this.currentResource(); - const headerTranslation = - resource?.resourceType === ResourceType.Project - ? 'project.overview.dialog.fork.headerProject' - : resource?.resourceType === ResourceType.Registration - ? 'project.overview.dialog.fork.headerRegistry' - : ''; - if (resource) { - this.dialogService.open(ForkDialogComponent, { - focusOnShow: false, - header: this.translateService.instant(headerTranslation), - closeOnEscape: true, - modal: true, - closable: true, - data: { - resource: resource, - }, - }); - } - } - - private handleDuplicateProject(): void { - this.dialogService.open(DuplicateDialogComponent, { - focusOnShow: false, - header: this.translateService.instant('project.overview.dialog.duplicate.header'), - closeOnEscape: true, - modal: true, - closable: true, - }); - } - protected handleToggleProjectPublicity(): void { const resource = this.currentResource(); if (!resource) return; @@ -210,4 +185,91 @@ export class OverviewToolbarComponent { }); } } + + private handleForkResource(): void { + const resource = this.currentResource(); + const headerTranslation = + resource?.resourceType === ResourceType.Project + ? 'project.overview.dialog.fork.headerProject' + : resource?.resourceType === ResourceType.Registration + ? 'project.overview.dialog.fork.headerRegistry' + : ''; + if (resource) { + this.dialogService.open(ForkDialogComponent, { + focusOnShow: false, + header: this.translateService.instant(headerTranslation), + closeOnEscape: true, + modal: true, + closable: true, + data: { + resource: resource, + }, + }); + } + } + + private handleDuplicateProject(): void { + this.dialogService.open(DuplicateDialogComponent, { + focusOnShow: false, + header: this.translateService.instant('project.overview.dialog.duplicate.header'), + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + private createShareableContent(): ShareableContent | null { + const resource = this.currentResource(); + const title = this.projectTitle(); + const description = this.projectDescription(); + + if (!resource?.isPublic || !title) { + return null; + } + + return { + id: resource.id, + title, + description, + url: this.buildResourceUrl(resource), + }; + } + + private buildResourceUrl(resource: ToolbarResource): string { + switch (resource.resourceType) { + case ResourceType.Project: + return this.socialShareService.createProjectUrl(resource.id); + case ResourceType.Registration: + return this.socialShareService.createRegistrationUrl(resource.id); + default: + return `${window.location.origin}/${resource.id}`; + } + } + + private buildSocialActionItems(shareableContent: ShareableContent): SocialsShareActionItem[] { + const shareLinks = this.socialShareService.generateAllSharingLinks(shareableContent); + + return [ + { + label: 'project.overview.actions.socials.email', + icon: 'fas fa-envelope', + url: shareLinks.email, + }, + { + label: 'project.overview.actions.socials.x', + icon: 'fab fa-x-twitter', + url: shareLinks.twitter, + }, + { + label: 'project.overview.actions.socials.linkedIn', + icon: 'fab fa-linkedin', + url: shareLinks.linkedIn, + }, + { + label: 'project.overview.actions.socials.facebook', + icon: 'fab fa-facebook-f', + url: shareLinks.facebook, + }, + ]; + } } diff --git a/src/app/features/project/overview/constants/index.ts b/src/app/features/project/overview/constants/index.ts deleted file mode 100644 index 63e607493..000000000 --- a/src/app/features/project/overview/constants/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './social-actions.constants'; diff --git a/src/app/features/project/overview/constants/social-actions.constants.ts b/src/app/features/project/overview/constants/social-actions.constants.ts deleted file mode 100644 index df2b43ef0..000000000 --- a/src/app/features/project/overview/constants/social-actions.constants.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const SOCIAL_ACTION_ITEMS = [ - { - label: 'project.overview.actions.socials.email', - icon: 'fas fa-envelope', - }, - { - label: 'project.overview.actions.socials.x', - icon: 'fab fa-x-twitter', - }, - { - label: 'project.overview.actions.socials.linkedIn', - icon: 'fab fa-linkedin', - }, - { - label: 'project.overview.actions.socials.facebook', - icon: 'fab fa-facebook-f', - }, -]; diff --git a/src/app/features/project/overview/models/index.ts b/src/app/features/project/overview/models/index.ts index 193301d9a..9dc9f174b 100644 --- a/src/app/features/project/overview/models/index.ts +++ b/src/app/features/project/overview/models/index.ts @@ -1 +1,2 @@ export * from './project-overview.models'; +export * from './socials-share-action-item.model'; diff --git a/src/app/features/project/overview/models/socials-share-action-item.model.ts b/src/app/features/project/overview/models/socials-share-action-item.model.ts new file mode 100644 index 000000000..f224bc613 --- /dev/null +++ b/src/app/features/project/overview/models/socials-share-action-item.model.ts @@ -0,0 +1,5 @@ +export interface SocialsShareActionItem { + label: string; + icon: string; + url: string; +} diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index 70c136e04..788b8b3a5 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -16,6 +16,8 @@ diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 804cfabdb..653dcb4cd 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -38,6 +38,7 @@ export * from './search'; export * from './select-option.model'; export * from './severity.type'; export * from './social-icon.model'; +export * from './social-share.model'; export * from './status-info.model'; export * from './step-option.model'; export * from './store'; diff --git a/src/app/shared/models/social-share.model.ts b/src/app/shared/models/social-share.model.ts new file mode 100644 index 000000000..8f1c1e3c0 --- /dev/null +++ b/src/app/shared/models/social-share.model.ts @@ -0,0 +1,13 @@ +export interface ShareableContent { + id: string; + title: string; + description?: string; + url: string; +} + +export interface SocialShareLinks { + email: string; + twitter: string; + facebook: string; + linkedIn: string; +} diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 41ca37cad..ebb526b6e 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -16,6 +16,7 @@ export { NodeLinksService } from './node-links.service'; export { RegionsService } from './regions.service'; export { ResourceCardService } from './resource-card.service'; export { SearchService } from './search.service'; +export { SocialShareService } from './social-share.service'; export { SubjectsService } from './subjects.service'; export { ToastService } from './toast.service'; export { ViewOnlyLinksService } from './view-only-links.service'; diff --git a/src/app/shared/services/social-share.service.ts b/src/app/shared/services/social-share.service.ts new file mode 100644 index 000000000..23faca5a1 --- /dev/null +++ b/src/app/shared/services/social-share.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; + +import { ShareableContent, SocialShareLinks } from '@shared/models/social-share.model'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class SocialShareService { + generateEmailLink(content: ShareableContent): string { + const subject = encodeURIComponent(content.title); + const body = encodeURIComponent(content.url); + + return `mailto:?subject=${subject}&body=${body}`; + } + + generateTwitterLink(content: ShareableContent): string { + const url = encodeURIComponent(content.url); + const text = encodeURIComponent(content.title); + + return `https://twitter.com/intent/tweet?url=${url}&text=${text}`; + } + + generateFacebookLink(content: ShareableContent): string { + const href = encodeURIComponent(content.url); + + return `https://www.facebook.com/sharer/sharer.php?u=${href}`; + } + + generateLinkedInLink(content: ShareableContent): string { + const url = encodeURIComponent(content.url); + const title = encodeURIComponent(content.title); + const summary = encodeURIComponent(content.description || content.title); + const source = encodeURIComponent('OSF'); + + return `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${title}&summary=${summary}&source=${source}`; + } + + generateAllSharingLinks(content: ShareableContent): SocialShareLinks { + return { + email: this.generateEmailLink(content), + twitter: this.generateTwitterLink(content), + facebook: this.generateFacebookLink(content), + linkedIn: this.generateLinkedInLink(content), + }; + } + + createPreprintUrl(preprintId: string, providerId: string): string { + return `${environment.webUrl}/preprints/${providerId}/${preprintId}`; + } + + createProjectUrl(projectId: string): string { + return `${environment.webUrl}/${projectId}`; + } + + createRegistrationUrl(registrationId: string, providerId = 'osf'): string { + return `${environment.webUrl}/registries/${providerId}/${registrationId}`; + } + + createDownloadUrl(resourceId: string): string { + return `${environment.webUrl}/download/${resourceId}`; + } +} From 99fbe7797c47a78d8ddd60a764db5cd1fbcf44c3 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Tue, 19 Aug 2025 17:03:46 +0300 Subject: [PATCH 2/2] feat(overview-social-share): moved social urls to separate config file --- src/app/shared/config/social-share.config.ts | 6 ++++++ src/app/shared/services/social-share.service.ts | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/config/social-share.config.ts diff --git a/src/app/shared/config/social-share.config.ts b/src/app/shared/config/social-share.config.ts new file mode 100644 index 000000000..8c9ca8167 --- /dev/null +++ b/src/app/shared/config/social-share.config.ts @@ -0,0 +1,6 @@ +export const SOCIAL_SHARE_URLS = { + email: 'mailto:', + twitter: 'https://twitter.com/intent/tweet', + facebook: 'https://www.facebook.com/sharer/sharer.php', + linkedIn: 'https://www.linkedin.com/shareArticle', +}; diff --git a/src/app/shared/services/social-share.service.ts b/src/app/shared/services/social-share.service.ts index 23faca5a1..ed2eb73c1 100644 --- a/src/app/shared/services/social-share.service.ts +++ b/src/app/shared/services/social-share.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; +import { SOCIAL_SHARE_URLS } from '@shared/config/social-share.config'; import { ShareableContent, SocialShareLinks } from '@shared/models/social-share.model'; import { environment } from 'src/environments/environment'; @@ -12,20 +13,20 @@ export class SocialShareService { const subject = encodeURIComponent(content.title); const body = encodeURIComponent(content.url); - return `mailto:?subject=${subject}&body=${body}`; + return `${SOCIAL_SHARE_URLS.email}?subject=${subject}&body=${body}`; } generateTwitterLink(content: ShareableContent): string { const url = encodeURIComponent(content.url); const text = encodeURIComponent(content.title); - return `https://twitter.com/intent/tweet?url=${url}&text=${text}`; + return `${SOCIAL_SHARE_URLS.twitter}?url=${url}&text=${text}`; } generateFacebookLink(content: ShareableContent): string { const href = encodeURIComponent(content.url); - return `https://www.facebook.com/sharer/sharer.php?u=${href}`; + return `${SOCIAL_SHARE_URLS.facebook}?u=${href}`; } generateLinkedInLink(content: ShareableContent): string { @@ -34,7 +35,7 @@ export class SocialShareService { const summary = encodeURIComponent(content.description || content.title); const source = encodeURIComponent('OSF'); - return `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${title}&summary=${summary}&source=${source}`; + return `${SOCIAL_SHARE_URLS.linkedIn}?mini=true&url=${url}&title=${title}&summary=${summary}&source=${source}`; } generateAllSharingLinks(content: ShareableContent): SocialShareLinks {