Added /api on frontend, identity banned users on profile with alert
This commit is contained in:
parent
f22b139e51
commit
1907c22b16
@ -28,6 +28,12 @@ data class UserDetails(
|
|||||||
val stolen_replays: Int = 0
|
val stolen_replays: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class UserDetailsExtended(
|
||||||
|
val userDetails: UserDetails,
|
||||||
|
val is_banned: Boolean = false,
|
||||||
|
val approximate_ban_date: OffsetDateTime? = null
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@AllowCacheSerialization
|
@AllowCacheSerialization
|
||||||
data class Statistics(
|
data class Statistics(
|
||||||
|
|||||||
@ -26,6 +26,8 @@ class UserDetailsController(
|
|||||||
val suspicious_scores: List<SuspiciousScoreEntry>,
|
val suspicious_scores: List<SuspiciousScoreEntry>,
|
||||||
val similar_replays: List<SimilarReplayEntry>,
|
val similar_replays: List<SimilarReplayEntry>,
|
||||||
val total_scores: Int = 0,
|
val total_scores: Int = 0,
|
||||||
|
val is_banned: Boolean,
|
||||||
|
val approximate_ban_date: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class UserQueueRequest(
|
data class UserQueueRequest(
|
||||||
@ -56,18 +58,22 @@ class UserDetailsController(
|
|||||||
|
|
||||||
@PostMapping("user-details")
|
@PostMapping("user-details")
|
||||||
fun getUserDetails(@RequestBody request: UserDetailsRequest): ResponseEntity<UserDetailsResponse> {
|
fun getUserDetails(@RequestBody request: UserDetailsRequest): ResponseEntity<UserDetailsResponse> {
|
||||||
val userDetails = this.userService.getUserDetails(username = request.userId)
|
val userDetailsExtended = this.userService.getUserDetails(username = request.userId)
|
||||||
?: return ResponseEntity.notFound().build()
|
?: return ResponseEntity.notFound().build()
|
||||||
|
|
||||||
|
val userId = userDetailsExtended.userDetails.user_id
|
||||||
|
|
||||||
var suspiciousScoresCondition = this.scoreService.getDefaultCondition()
|
var suspiciousScoresCondition = this.scoreService.getDefaultCondition()
|
||||||
suspiciousScoresCondition = suspiciousScoresCondition.and(SCORES.USER_ID.eq(userDetails.user_id))
|
suspiciousScoresCondition = suspiciousScoresCondition.and(SCORES.USER_ID.eq(userId))
|
||||||
|
|
||||||
val response = UserDetailsResponse(
|
val response = UserDetailsResponse(
|
||||||
user_details = userDetails,
|
user_details = userDetailsExtended.userDetails,
|
||||||
queue_details = this.userQueueService.getUserQueueDetails(userDetails.user_id),
|
queue_details = this.userQueueService.getUserQueueDetails(userId),
|
||||||
suspicious_scores = this.scoreService.getSuspiciousScores(suspiciousScoresCondition),
|
suspicious_scores = this.scoreService.getSuspiciousScores(suspiciousScoresCondition),
|
||||||
similar_replays = this.scoreService.getSimilarReplaysForUserId(userDetails.user_id),
|
similar_replays = this.scoreService.getSimilarReplaysForUserId(userId),
|
||||||
total_scores = this.userService.getTotalUserScores(userDetails.user_id)
|
total_scores = this.userService.getTotalUserScores(userId),
|
||||||
|
is_banned = userDetailsExtended.is_banned,
|
||||||
|
approximate_ban_date = userDetailsExtended.approximate_ban_date?.toString()
|
||||||
)
|
)
|
||||||
return ResponseEntity.ok(response)
|
return ResponseEntity.ok(response)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
package com.nisemoe.nise.database
|
package com.nisemoe.nise.database
|
||||||
|
|
||||||
import com.nisemoe.generated.tables.records.UpdateUserQueueRecord
|
|
||||||
import com.nisemoe.generated.tables.records.UsersRecord
|
import com.nisemoe.generated.tables.records.UsersRecord
|
||||||
import com.nisemoe.generated.tables.references.SCORES
|
import com.nisemoe.generated.tables.references.SCORES
|
||||||
import com.nisemoe.generated.tables.references.UPDATE_USER_QUEUE
|
|
||||||
import com.nisemoe.generated.tables.references.USERS
|
import com.nisemoe.generated.tables.references.USERS
|
||||||
import com.nisemoe.nise.Format
|
import com.nisemoe.nise.Format
|
||||||
import com.nisemoe.nise.UserDetails
|
import com.nisemoe.nise.UserDetails
|
||||||
import com.nisemoe.nise.UserQueueDetails
|
import com.nisemoe.nise.UserDetailsExtended
|
||||||
import com.nisemoe.nise.osu.OsuApi
|
import com.nisemoe.nise.osu.OsuApi
|
||||||
import com.nisemoe.nise.osu.OsuApiModels
|
import com.nisemoe.nise.osu.OsuApiModels
|
||||||
import org.jooq.DSLContext
|
import org.jooq.DSLContext
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
@ -74,13 +71,13 @@ class UserService(
|
|||||||
return this.mapUserToDatabase(apiUser)
|
return this.mapUserToDatabase(apiUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUserDetails(username: String): UserDetails? {
|
fun getUserDetails(username: String): UserDetailsExtended? {
|
||||||
val user = dslContext.selectFrom(USERS)
|
val user = dslContext.selectFrom(USERS)
|
||||||
.where(USERS.USERNAME.equalIgnoreCase(username.lowercase()))
|
.where(USERS.USERNAME.equalIgnoreCase(username.lowercase()))
|
||||||
.fetchOneInto(UsersRecord::class.java)
|
.fetchOneInto(UsersRecord::class.java)
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
return UserDetails(
|
val userDetails = UserDetails(
|
||||||
user.userId!!,
|
user.userId!!,
|
||||||
user.username!!,
|
user.username!!,
|
||||||
user.rank,
|
user.rank,
|
||||||
@ -91,6 +88,11 @@ class UserService(
|
|||||||
user.countryRank,
|
user.countryRank,
|
||||||
user.playcount
|
user.playcount
|
||||||
)
|
)
|
||||||
|
return UserDetailsExtended(
|
||||||
|
userDetails = userDetails,
|
||||||
|
is_banned = user.isBanned ?: false,
|
||||||
|
approximate_ban_date = user.approxBanDate
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The database does NOT have the user; we will now use the osu!api
|
// The database does NOT have the user; we will now use the osu!api
|
||||||
@ -100,7 +102,9 @@ class UserService(
|
|||||||
// Persist to database
|
// Persist to database
|
||||||
insertApiUser(apiUser)
|
insertApiUser(apiUser)
|
||||||
|
|
||||||
return this.mapUserToDatabase(apiUser)
|
return UserDetailsExtended(
|
||||||
|
userDetails = this.mapUserToDatabase(apiUser)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertApiUser(apiUser: OsuApiModels.UserExtended) {
|
fun insertApiUser(apiUser: OsuApiModels.UserExtended) {
|
||||||
|
|||||||
@ -4,13 +4,19 @@
|
|||||||
<p>
|
<p>
|
||||||
if you'd like to retrieve data from our database, you are invited to use the same endpoints meant for the frontend but in a programmatic way. <u>currently there's no rate limits.</u>
|
if you'd like to retrieve data from our database, you are invited to use the same endpoints meant for the frontend but in a programmatic way. <u>currently there's no rate limits.</u>
|
||||||
</p>
|
</p>
|
||||||
|
<div class="alert mb-2 text-center">
|
||||||
|
<p>
|
||||||
|
if you have any issues related to CloudFlare, let me know on discord. currently all api requests are routed via CF so it's possible it might block certain IPs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<p>as of today, you MUST pass the following parameters:</p>
|
<p>as of today, you MUST pass the following parameters:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>The <code>X-NISE-API</code> header with a value of <code>20240218</code> in every request.</li>
|
<li>The <code>X-NISE-API</code> header with a value of <code>20240218</code> in every request. This should prevent random shit from breaking in the future (if you keep passing it)</li>
|
||||||
<li>The <code>Accept</code> header with a value of <code>application/json</code> (if you want JSON instead of XML)</li>
|
<li>The <code>Accept</code> header with a value of <code>application/json</code> (if you want JSON instead of XML)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h1 class="mt-4">## scores search</h1>
|
<h1 class="mt-4">## scores search</h1>
|
||||||
<p>score search is based on predicates. a predicate is a list of specifications/conditions to match results. predicates can be combined with operators such as <code>AND</code> and <code>OR</code></p>
|
<p>score search is based on predicates. a predicate is a list of specifications/conditions to match results. predicates can be combined with operators such as <code>AND</code> and <code>OR</code></p>
|
||||||
|
<p style="font-weight: bold; color: orange">COMING SOON; the route exists but its cancer for api users to form the requests.</p>
|
||||||
|
|
||||||
<h1 class="mt-4">## get single score</h1>
|
<h1 class="mt-4">## get single score</h1>
|
||||||
<p>if you have a <code>replay_id</code>, you can retrieve all the information 'bout that score.</p>
|
<p>if you have a <code>replay_id</code>, you can retrieve all the information 'bout that score.</p>
|
||||||
@ -19,7 +25,110 @@
|
|||||||
<li><strong>METHOD:</strong> GET</li>
|
<li><strong>METHOD:</strong> GET</li>
|
||||||
</ul>
|
</ul>
|
||||||
Example:
|
Example:
|
||||||
<code style="font-size: 12px">curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439</code>
|
<br>
|
||||||
|
<app-code-with-copy-button>
|
||||||
|
curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439
|
||||||
|
</app-code-with-copy-button>
|
||||||
|
|
||||||
|
<h1 class="mt-4">## get single replay (data)</h1>
|
||||||
|
<p>if you have a <code>replay_id</code>, you can retrieve the data that is used for the replay viewer <i>(replay.nise.moe)</i>
|
||||||
|
<p>it contains the beatmap data, the full replay data, and all calculated judgements (by circleguard)</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>ENDPOINT:</strong> <code>/api/score/{replay_id}/replay</code></li>
|
||||||
|
<li><strong>METHOD:</strong> GET</li>
|
||||||
|
</ul>
|
||||||
|
Example:
|
||||||
|
<br>
|
||||||
|
<app-code-with-copy-button>
|
||||||
|
curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439/replay
|
||||||
|
</app-code-with-copy-button>
|
||||||
|
|
||||||
|
<h1 class="mt-4">## get single replay (.osr)</h1>
|
||||||
|
<p>if you have a <code>replay_id</code>, you can retrieve the <i>emulated</i> .osr replay file.</p>
|
||||||
|
<p><i>wtf is an emulated .osr replay file?</i> 🡒 the osu!api does NOT return a "real" replay, so we fill the rest of the details.</p>
|
||||||
|
<p style="font-weight: bold; color: orange">COMING SOON</p>
|
||||||
|
|
||||||
|
<h1 class="mt-4">## get user details</h1>
|
||||||
|
<p>if you have an <code>user_id</code>, you can retrieve everything we know 'bout that user.
|
||||||
|
<p style="font-weight: bold; color: #fa5c5c">>> <strong>user_id</strong> is the username. sry.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>ENDPOINT:</strong> <code>/api/user-details</code></li>
|
||||||
|
<li><strong>METHOD:</strong> POST</li>
|
||||||
|
<li><strong>POST FIELDS:</strong> user_id: str (*required)</li>
|
||||||
|
<li><strong>POST FORMAT:</strong> JSON only</li>
|
||||||
|
</ul>
|
||||||
|
Example:
|
||||||
|
<br>
|
||||||
|
<app-code-with-copy-button>
|
||||||
|
curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"userId": "degenerate"}' https://nise.moe/api/user-details
|
||||||
|
</app-code-with-copy-button>
|
||||||
|
|
||||||
|
<h1 class="mt-4">## get suspicious scores</h1>
|
||||||
|
<p>returns a list of suspicious scores (e.g. cvUR <= 25, basically <span style="font-size: 18px" class="board">/sus/</span>)</p>
|
||||||
|
<p>it is not <i>live</i>, but rather updates every N minutes depending on server load (usually ~10 minutes)</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>ENDPOINT:</strong> <code>/api/suspicious-scores</code></li>
|
||||||
|
<li><strong>METHOD:</strong> GET</li>
|
||||||
|
</ul>
|
||||||
|
Example:
|
||||||
|
<br>
|
||||||
|
<app-code-with-copy-button>
|
||||||
|
curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/suspicious-scores
|
||||||
|
</app-code-with-copy-button>
|
||||||
|
|
||||||
|
<h1 class="mt-4">## get stolen replays</h1>
|
||||||
|
<p>returns a list of all replay pairs with < 10 similarity ratio (basically <span style="font-size: 18px" class="board">/nu/</span>)</p>
|
||||||
|
<p>it is not <i>live</i>, but rather updates every N minutes depending on server load (usually ~10 minutes)</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>ENDPOINT:</strong> <code>/api/similar-replays</code></li>
|
||||||
|
<li><strong>METHOD:</strong> GET</li>
|
||||||
|
</ul>
|
||||||
|
Example:
|
||||||
|
<br>
|
||||||
|
<app-code-with-copy-button>
|
||||||
|
curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/similar-replays
|
||||||
|
</app-code-with-copy-button>
|
||||||
|
|
||||||
|
<h1 class="mt-4">## get banlist</h1>
|
||||||
|
<p>returns a list of all <i>possibly<strong>*</strong></i> banned users<br><strong>*</strong>(so users for which the osu!web page returned the <code>User not found! ;_;</code> response.)</p>
|
||||||
|
<p style="font-weight: bold; color: #ff6dff">>> page size aint configurable, it's 100 atm. maybe a TODO.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>ENDPOINT:</strong> <code>/api/banlist</code></li>
|
||||||
|
<li><strong>METHOD:</strong> POST</li>
|
||||||
|
<li><strong>POST FIELDS:</strong> page: int (*required, starts at 1)</li>
|
||||||
|
<li><strong>POST FORMAT:</strong> JSON only</li>
|
||||||
|
</ul>
|
||||||
|
Example:
|
||||||
|
<br>
|
||||||
|
<app-code-with-copy-button>
|
||||||
|
curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"page": 1}' https://nise.moe/api/banlist
|
||||||
|
</app-code-with-copy-button>
|
||||||
|
|
||||||
|
<h1 class="mt-4">## analyze replay file (.osr)</h1>
|
||||||
|
<p>if you have an .osr file, you can analyze it and get results + an always-online static page for it.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>ENDPOINT:</strong> <code>/api/analyze</code></li>
|
||||||
|
<li><strong>METHOD:</strong> POST</li>
|
||||||
|
<li><strong>POST FIELDS:</strong> replay: file (*required, an .osr file)</li>
|
||||||
|
</ul>
|
||||||
|
<app-code-with-copy-button>
|
||||||
|
curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -F "replay=@replay1.osr" https://nise.moe/api/analyze
|
||||||
|
</app-code-with-copy-button>
|
||||||
|
<p>the response will include an <code>id</code> parameter, which identifies the replay you've uploaded.</p>
|
||||||
|
<p>that id will be subsequently available at:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>WEB INTERFACE</strong>: https://nise.moe/c/{id}</li>
|
||||||
|
<li><strong>API:</strong> https://nise.moe/api/user-scores/{id}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="mb-2 text-center">
|
||||||
|
<p>
|
||||||
|
thanks for using the api. if you have any issues or requests, let me know on discord.
|
||||||
|
</p>
|
||||||
|
<p><strong>plz,</strong> try to not abuse it too much.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import { Component } from '@angular/core';
|
|||||||
import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe";
|
import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe";
|
||||||
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
|
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
|
||||||
import {DatePipe, DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
import {DatePipe, DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
||||||
|
import {
|
||||||
|
CodeWithCopyButtonComponent
|
||||||
|
} from "../../corelib/components/code-with-copy-button/code-with-copy-button.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-api',
|
selector: 'app-api',
|
||||||
@ -12,7 +15,8 @@ import {DatePipe, DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
|||||||
DatePipe,
|
DatePipe,
|
||||||
DecimalPipe,
|
DecimalPipe,
|
||||||
NgForOf,
|
NgForOf,
|
||||||
NgIf
|
NgIf,
|
||||||
|
CodeWithCopyButtonComponent
|
||||||
],
|
],
|
||||||
templateUrl: './api.component.html',
|
templateUrl: './api.component.html',
|
||||||
styleUrl: './api.component.css'
|
styleUrl: './api.component.css'
|
||||||
|
|||||||
@ -35,5 +35,5 @@
|
|||||||
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
<div class="text-center version">
|
<div class="text-center version">
|
||||||
v20240508
|
v20240510
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,6 +19,17 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<ng-container *ngIf="this.userInfo.is_banned && this.userInfo.approximate_ban_date">
|
||||||
|
|
||||||
|
<div class="alert alert-danger mb-2 text-center">
|
||||||
|
<p>
|
||||||
|
uh oh! this user <i>might</i> be banned.
|
||||||
|
</p>
|
||||||
|
<p>last time ({{ this.userInfo.approximate_ban_date | date: 'medium' }}) we checked their osu!web profile,<br> we got the dreaded <code>User not found! ;_;</code> response.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<div class="mb-2 mt-2 btn-group">
|
<div class="mb-2 mt-2 btn-group">
|
||||||
<a [href]="'https://osu.ppy.sh/users/' + this.userInfo.user_details.user_id + '/osu'"
|
<a [href]="'https://osu.ppy.sh/users/' + this.userInfo.user_details.user_id + '/osu'"
|
||||||
class="btn btn-outline-secondary btn-sm" target="_blank">
|
class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {SimilarReplay, SuspiciousScore} from "../replays";
|
|||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from "@angular/common/http";
|
||||||
import {catchError, EMPTY, finalize, Observable, Subscription} from "rxjs";
|
import {catchError, EMPTY, finalize, Observable, Subscription} from "rxjs";
|
||||||
import {environment} from "../../environments/environment";
|
import {environment} from "../../environments/environment";
|
||||||
import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
|
import {DatePipe, DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
|
||||||
import {ActivatedRoute, RouterLink} from "@angular/router";
|
import {ActivatedRoute, RouterLink} from "@angular/router";
|
||||||
import {UserDetails, UserQueueDetails} from "../userDetails";
|
import {UserDetails, UserQueueDetails} from "../userDetails";
|
||||||
import {calculateTimeAgo, countryCodeToFlag, formatDuration} from "../format";
|
import {calculateTimeAgo, countryCodeToFlag, formatDuration} from "../format";
|
||||||
@ -21,6 +21,8 @@ interface UserInfo {
|
|||||||
suspicious_scores: SuspiciousScore[];
|
suspicious_scores: SuspiciousScore[];
|
||||||
similar_replays: SimilarReplay[];
|
similar_replays: SimilarReplay[];
|
||||||
total_scores: number;
|
total_scores: number;
|
||||||
|
is_banned: boolean;
|
||||||
|
approximate_ban_date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserQueueWebsocketPacket {
|
interface UserQueueWebsocketPacket {
|
||||||
@ -42,7 +44,8 @@ interface UserScoresFilter {
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
NgIf,
|
NgIf,
|
||||||
NgOptimizedImage,
|
NgOptimizedImage,
|
||||||
CuteLoadingComponent
|
CuteLoadingComponent,
|
||||||
|
DatePipe
|
||||||
],
|
],
|
||||||
templateUrl: './view-user.component.html',
|
templateUrl: './view-user.component.html',
|
||||||
styleUrl: './view-user.component.css'
|
styleUrl: './view-user.component.css'
|
||||||
|
|||||||
@ -200,6 +200,11 @@ a.btn-success:hover {
|
|||||||
border: 1px dotted #b3b8c3;
|
border: 1px dotted #b3b8c3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
color: #de7979;
|
||||||
|
border-color: #de7979;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-2 {
|
.mb-2 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
<ng-container *ngIf="this.textToCopy; else ngContent">
|
||||||
|
<code style="font-size: 12px; margin-right: 10px">{{ this.textToCopy }}</code> <button (click)="copyToClipboard()">copy</button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #ngContent>
|
||||||
|
<ng-content ></ng-content>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input} from '@angular/core';
|
||||||
|
import {NgIf} from "@angular/common";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-code-with-copy-button',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './code-with-copy-button.component.html',
|
||||||
|
styleUrl: './code-with-copy-button.component.css'
|
||||||
|
})
|
||||||
|
export class CodeWithCopyButtonComponent implements AfterViewInit {
|
||||||
|
|
||||||
|
@Input() textToCopy!: string;
|
||||||
|
|
||||||
|
constructor(private elRef: ElementRef, private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.textToCopy = this.elRef.nativeElement.innerText.trim();
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToClipboard() {
|
||||||
|
navigator.clipboard.writeText(this.textToCopy).then(() => {
|
||||||
|
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('could not copy text:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user