Merge branch 'main' into cal/dev-124-project-validation
This commit is contained in:
commit
89351b4be4
2
.github/workflows/turbo-ci.yml
vendored
2
.github/workflows/turbo-ci.yml
vendored
@ -80,4 +80,4 @@ jobs:
|
||||
- name: 🔍 Verify intl:extract has been run
|
||||
run: |
|
||||
pnpm intl:extract
|
||||
git diff --exit-code */*/src/locales/en-US/index.json
|
||||
git diff --exit-code --color */*/src/locales/en-US/index.json
|
||||
|
||||
@ -67,9 +67,8 @@ export async function determineModelType(texture: string): Promise<'SLIM' | 'CLA
|
||||
const armWidth = 2
|
||||
const armHeight = 12
|
||||
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
||||
for (let index = 1; index <= imageData.length; index++) {
|
||||
//every fourth value in RGBA is the alpha channel
|
||||
if (index % 4 == 0 && imageData[index - 1] !== 0) {
|
||||
for (let alphaIndex = 3; alphaIndex < imageData.length; alphaIndex += 4) {
|
||||
if (imageData[alphaIndex] !== 0) {
|
||||
resolve('CLASSIC')
|
||||
return
|
||||
}
|
||||
|
||||
@ -220,7 +220,7 @@ async function refreshSearch() {
|
||||
}
|
||||
}
|
||||
results.value = rawResults.result
|
||||
currentPage.value = Math.max(1, Math.min(pageCount.value, currentPage.value))
|
||||
currentPage.value = 1
|
||||
|
||||
const persistentParams: LocationQuery = {}
|
||||
|
||||
@ -266,6 +266,7 @@ async function onSearchChangeToTop() {
|
||||
|
||||
function clearSearch() {
|
||||
query.value = ''
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@ -150,9 +150,26 @@
|
||||
</template>
|
||||
</span>
|
||||
<span class="text-sm text-secondary">
|
||||
<span
|
||||
v-if="charge.status === 'cancelled' && $dayjs(charge.due).isBefore($dayjs())"
|
||||
class="font-bold"
|
||||
>
|
||||
Ended:
|
||||
</span>
|
||||
<span v-else-if="charge.status === 'cancelled'" class="font-bold">Ends:</span>
|
||||
<span v-else-if="charge.type === 'refund'" class="font-bold">Issued:</span>
|
||||
<span v-else class="font-bold">Due:</span>
|
||||
{{ dayjs(charge.due).format("MMMM D, YYYY [at] h:mma") }}
|
||||
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
|
||||
</span>
|
||||
<span v-if="charge.last_attempt != null" class="text-sm text-secondary">
|
||||
<span v-if="charge.status === 'failed'" class="font-bold">Last attempt:</span>
|
||||
<span v-else class="font-bold">Charged:</span>
|
||||
{{ dayjs(charge.last_attempt).format("MMMM D, YYYY [at] h:mma") }}
|
||||
<span class="text-secondary"
|
||||
>({{ formatRelativeTime(charge.last_attempt) }})
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex w-full items-center gap-1 text-xs text-secondary">
|
||||
{{ charge.status }}
|
||||
⋅
|
||||
|
||||
@ -45,8 +45,9 @@
|
||||
<h2
|
||||
class="relative m-0 max-w-2xl text-base font-normal leading-[155%] text-secondary md:text-[1.2rem]"
|
||||
>
|
||||
Modrinth Servers is the easiest way to host your own Minecraft server. Seamlessly install
|
||||
and play your favorite mods and modpacks, all within the Modrinth platform.
|
||||
Modrinth Servers is the easiest way to host your own Minecraft: Java Edition server.
|
||||
Seamlessly install and play your favorite mods and modpacks, all within the Modrinth
|
||||
platform.
|
||||
</h2>
|
||||
<div class="relative flex w-full flex-wrap items-center gap-8 align-middle sm:w-fit">
|
||||
<div
|
||||
@ -459,7 +460,7 @@
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details pyro-hash="players" class="group" :open="$route.hash === '#players'">
|
||||
<details pyro-hash="performance" class="group" :open="$route.hash === '#performance'">
|
||||
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
||||
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
||||
<RightArrowIcon />
|
||||
@ -480,7 +481,7 @@
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details pyro-hash="players" class="group" :open="$route.hash === '#prices'">
|
||||
<details pyro-hash="prices" class="group" :open="$route.hash === '#prices'">
|
||||
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
||||
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
||||
<RightArrowIcon />
|
||||
@ -491,6 +492,24 @@
|
||||
All prices are listed in United States Dollars (USD).
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details pyro-hash="versions" class="group" :open="$route.hash === '#versions'">
|
||||
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
||||
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
||||
<RightArrowIcon />
|
||||
</span>
|
||||
What Minecraft versions and loaders can be used?
|
||||
</summary>
|
||||
<p class="m-0 ml-6 leading-[160%]">
|
||||
Modrinth Servers can run any version of Minecraft: Java Edition going all the way
|
||||
back to version 1.2.5, including snapshot versions.
|
||||
</p>
|
||||
<p class="m-0 ml-6 mt-3 leading-[160%]">
|
||||
We also support a wide range of mod and plugin loaders, including Fabric, Quilt,
|
||||
Forge, and NeoForge for mods, as well as Paper and Purpur for plugins. Availability
|
||||
depends on whether the mod or plugin loader supports the selected Minecraft version.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')",
|
||||
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled' OR status = 'failed')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -102,5 +102,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "99cca53fd3f35325e2da3b671532bf98b8c7ad8e7cb9158e4eb9c5bac66d20b2"
|
||||
"hash": "cf020daa52a1316e5f60d197196b880b72c0b2a576e470d9fd7182558103d055"
|
||||
}
|
||||
@ -43,7 +43,9 @@ pub enum AuthenticationError {
|
||||
InvalidAuthMethod,
|
||||
#[error("GitHub Token from incorrect Client ID")]
|
||||
InvalidClientId,
|
||||
#[error("User email/account is already registered on Modrinth")]
|
||||
#[error(
|
||||
"User email is already registered on Modrinth. Try 'Forgot password' to access your account."
|
||||
)]
|
||||
DuplicateUser,
|
||||
#[error("Invalid state sent, you probably need to get a new websocket")]
|
||||
SocketError,
|
||||
|
||||
@ -197,7 +197,7 @@ impl DBCharge {
|
||||
) -> Result<Option<DBCharge>, DatabaseError> {
|
||||
let user_subscription_id = user_subscription_id.0;
|
||||
let res = select_charges_with_predicate!(
|
||||
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')",
|
||||
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled' OR status = 'failed')",
|
||||
user_subscription_id
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
|
||||
@ -223,8 +223,8 @@ impl TempUser {
|
||||
stripe_customer_id: None,
|
||||
totp_secret: None,
|
||||
username,
|
||||
email: self.email,
|
||||
email_verified: true,
|
||||
email: self.email.clone(),
|
||||
email_verified: self.email.is_some(),
|
||||
avatar_url,
|
||||
raw_avatar_url,
|
||||
bio: self.bio,
|
||||
@ -1419,15 +1419,15 @@ pub async fn create_account_with_password(
|
||||
.hash_password(new_account.password.as_bytes(), &salt)?
|
||||
.to_string();
|
||||
|
||||
if crate::database::models::DBUser::get_by_email(
|
||||
if !crate::database::models::DBUser::get_by_case_insensitive_email(
|
||||
&new_account.email,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.is_some()
|
||||
.is_empty()
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Email is already registered on Modrinth!".to_string(),
|
||||
"Email is already registered on Modrinth! Try 'Forgot password' to access your account.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -2220,6 +2220,18 @@ pub async fn set_email(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
if !crate::database::models::DBUser::get_by_case_insensitive_email(
|
||||
&email.email,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.is_empty()
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Email is already registered on Modrinth! Try 'Forgot password' in incognito to access and delete your other account.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"app:intl:extract": "pnpm run --filter=@modrinth/app-frontend intl:extract",
|
||||
"blog:fix": "turbo run fix --filter=@modrinth/blog",
|
||||
"pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
|
||||
"moderation:fix": "turbo run fix --filter=@modrinth/moderation",
|
||||
"build": "turbo run build --continue",
|
||||
"lint": "turbo run lint --continue",
|
||||
"test": "turbo run test --continue",
|
||||
|
||||
@ -284,6 +284,12 @@ async fn import_mmc_unmanaged(
|
||||
component.version.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
if component.uid.starts_with("net.neoforged") {
|
||||
return Some((
|
||||
PackDependency::NeoForge,
|
||||
component.version.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
if component.uid.starts_with("org.quiltmc.quilt-loader") {
|
||||
return Some((
|
||||
PackDependency::QuiltLoader,
|
||||
|
||||
@ -1 +0,0 @@
|
||||
**License id:** %PROJECT_LICENSE_ID% \
|
||||
@ -1 +1,2 @@
|
||||
**License id:** %PROJECT_LICENSE_ID% \
|
||||
**License Link:** %PROJECT_LICENSE_URL%
|
||||
@ -1 +1 @@
|
||||
> **{PLATFORM}:** {URL}
|
||||
> **{PLATFORM}:** {URL}<br />
|
||||
|
||||
@ -1 +1,2 @@
|
||||
<u>**Donation Links:**</u>
|
||||
<br />
|
||||
<u>**Donation Links:**</u><br />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
## No English Description
|
||||
|
||||
Per section 2.2 of %RULES% a project's Summary and Description must be in English, unless meant exclusively for non-English use, such as translations.
|
||||
Per section 2.2 of %RULES% a project's [Summary](%PROJECT_SETTINGS_LINK%) and %PROJECT_DESCRIPTION_FLINK% must be in English, unless meant exclusively for non-English use, such as translations.
|
||||
|
||||
You may include your non-English Description if you would like but we ask that you also add an English translation of the Description to your Description page, if you would like to use an online translator to do this, we recommend [DeepL](https://www.deepl.com/translator).
|
||||
You may include your non-English Description if you would like but we ask that you also add an English translation of the Description to your project page, if you would like to use an online translator to do this, we recommend [DeepL](https://www.deepl.com/translator).
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
## Identity Verification
|
||||
|
||||
**Welcome to Modrinth!** We're happy to see you here, we just want to make sure you're you.
|
||||
|
||||
Since this project already exists on the internet we ask that you provide evidence you are the rightful owner of this content.
|
||||
For instance, by submitting a screenshot accessing the settings of the project's existing pages such as %PLATFORM%.
|
||||
@ -0,0 +1,5 @@
|
||||
## Source Code Requested
|
||||
|
||||
To ensure the safety of all Modrinth users, we ask that you provide the source code for this project before resubmission so that it can be reviewed by our Moderation Team.
|
||||
We also ask that you provide the source for any included binary files, as well as detailed build instructions allowing us to verify that the compiled code you are distributing matches the provided source.
|
||||
We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub.
|
||||
@ -0,0 +1,4 @@
|
||||
## Source Code Requested
|
||||
|
||||
To ensure the safety of all Modrinth users, we ask that you provide the source code for this project and the process you used to obfuscate it before resubmission so that it can be reviewed by our Moderation Team.
|
||||
We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub.
|
||||
@ -20,14 +20,7 @@ const licensesNotRequiringSource: string[] = [
|
||||
|
||||
const licenseStage: Stage = {
|
||||
title: 'Is this license and link valid?',
|
||||
text: async (project) => {
|
||||
let text = ''
|
||||
text += (await import('../messages/checklist-text/license/id.md?raw')).default
|
||||
if (project.license.url)
|
||||
text += (await import('../messages/checklist-text/license/link.md?raw')).default
|
||||
|
||||
return text
|
||||
},
|
||||
text: async () => (await import('../messages/checklist-text/licensing.md?raw')).default,
|
||||
id: 'license',
|
||||
icon: BookTextIcon,
|
||||
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
|
||||
@ -55,33 +48,24 @@ const licenseStage: Stage = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// id: 'license_no_source',
|
||||
// type: 'conditional-button',
|
||||
// label: 'No Source',
|
||||
// fallbackWeight: 602,
|
||||
// suggestedStatus: 'rejected',
|
||||
// severity: 'medium',
|
||||
// fallbackMessage: async () => (await import('../messages/license/no_source.md?raw')).default,
|
||||
// messageVariants: [
|
||||
// {
|
||||
// conditions: {
|
||||
// requiredActions: ['reupload_unclear_fork'],
|
||||
// },
|
||||
// weight: 602,
|
||||
// message: async () => (await import('../messages/license/no_source-fork.md?raw')).default,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
id: 'license_no_source',
|
||||
type: 'button',
|
||||
type: 'conditional-button',
|
||||
label: 'No Source',
|
||||
weight: 602,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'medium',
|
||||
shouldShow: (project) => !licensesNotRequiringSource.includes(project.license.id),
|
||||
message: async () => (await import('../messages/license/no_source.md?raw')).default,
|
||||
messageVariants: [
|
||||
{
|
||||
conditions: {
|
||||
excludedActions: ['license_no_source-fork'],
|
||||
},
|
||||
weight: 602,
|
||||
message: async () => (await import('../messages/license/no_source.md?raw')).default,
|
||||
},
|
||||
],
|
||||
fallbackWeight: 602,
|
||||
fallbackMessage: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'license_no_source-fork',
|
||||
|
||||
@ -61,7 +61,8 @@ const links: Stage = {
|
||||
{
|
||||
id: 'links_unaccessible_options',
|
||||
type: 'multi-select-chips',
|
||||
label: 'Warn of unaccessible link?',
|
||||
label: 'Warn of inaccessible link?',
|
||||
shouldShow: (project) => Boolean(project.source_url || project.discord_url),
|
||||
options: [
|
||||
{
|
||||
label: 'Source',
|
||||
|
||||
@ -20,6 +20,7 @@ const reupload: Stage = {
|
||||
'reupload_unclear_fork',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
relevantExtraInput: [
|
||||
{
|
||||
@ -44,11 +45,12 @@ const reupload: Stage = {
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/fork.md?raw')).default,
|
||||
// disablesActions: [
|
||||
// 'reupload_reupload',
|
||||
// 'reupload_insufficient_fork',
|
||||
// 'reupload_request_proof',
|
||||
// ],
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_insufficient_fork',
|
||||
@ -58,7 +60,12 @@ const reupload: Stage = {
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () => (await import('../messages/reupload/insufficient_fork.md?raw')).default,
|
||||
// disablesActions: ['reupload_unclear_fork', 'reupload_reupload', 'reupload_request_proof'],
|
||||
disablesActions: [
|
||||
'reupload_unclear_fork',
|
||||
'reupload_reupload',
|
||||
'reupload_request_proof',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'reupload_request_proof',
|
||||
@ -69,7 +76,34 @@ const reupload: Stage = {
|
||||
severity: 'high',
|
||||
message: async () =>
|
||||
(await import('../messages/reupload/proof_of_permissions.md?raw')).default,
|
||||
// disablesActions: ['reupload_reupload', 'reupload_unclear_fork', 'reupload_insufficient_fork'],
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_unclear_fork',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_identity_verification',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'reupload_identity_verification',
|
||||
type: 'button',
|
||||
label: 'Verify Identity',
|
||||
weight: 1100,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
message: async () =>
|
||||
(await import('../messages/reupload/identity_verification.md?raw')).default,
|
||||
relevantExtraInput: [
|
||||
{
|
||||
label: 'Where else can the project be found?',
|
||||
variable: 'PLATFORM',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
disablesActions: [
|
||||
'reupload_reupload',
|
||||
'reupload_insufficient_fork',
|
||||
'reupload_request_proof',
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Stage } from '../../types/stage'
|
||||
import type { ButtonAction } from '../../types/actions'
|
||||
import type { ButtonAction, DropdownAction, DropdownActionOption } from '../../types/actions'
|
||||
import { TriangleAlertIcon } from '@modrinth/assets'
|
||||
|
||||
const statusAlerts: Stage = {
|
||||
@ -39,6 +39,41 @@ const statusAlerts: Stage = {
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/account_issues.md?raw')).default,
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_tec_source_request',
|
||||
type: 'button',
|
||||
label: `Request Source`,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'critical',
|
||||
disablesActions: ['status_corrections_applied', 'status_private_use'],
|
||||
shouldShow: (project) =>
|
||||
project.project_type === 'mod' ||
|
||||
project.project_type === 'shader' ||
|
||||
project.project_type.toString() === 'plugin',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'status_tec_source_request_options',
|
||||
type: 'dropdown',
|
||||
label: 'Why are you requesting source?',
|
||||
options: [
|
||||
{
|
||||
label: 'Obfuscated',
|
||||
weight: 999999,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/tec/source_request-obfs.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
{
|
||||
label: 'Binaries',
|
||||
weight: 999000,
|
||||
message: async () =>
|
||||
(await import('../messages/status-alerts/tec/source_request-bins.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
],
|
||||
} as DropdownAction,
|
||||
],
|
||||
} as ButtonAction,
|
||||
{
|
||||
id: 'status_automod_confusion',
|
||||
type: 'button',
|
||||
|
||||
@ -25,6 +25,8 @@ const versions: Stage = {
|
||||
label: 'Incorrect Project Type',
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'medium',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'versions_incorrect_project_type_options',
|
||||
@ -62,6 +64,8 @@ const versions: Stage = {
|
||||
label: 'Alternate Versions',
|
||||
suggestedStatus: 'flagged',
|
||||
severity: 'medium',
|
||||
weight: -999999,
|
||||
message: async () => '',
|
||||
enablesActions: [
|
||||
{
|
||||
id: 'versions_incorrect_project_type_options',
|
||||
@ -110,7 +114,7 @@ const versions: Stage = {
|
||||
weight: 1002,
|
||||
suggestedStatus: 'rejected',
|
||||
severity: 'high',
|
||||
shouldShow: (project) => project.project_type === `modpack`,
|
||||
shouldShow: (project) => project.project_type === 'modpack',
|
||||
message: async () =>
|
||||
(await import('../messages/versions/alternate_versions-zip.md?raw')).default,
|
||||
} as DropdownActionOption,
|
||||
|
||||
@ -10,6 +10,13 @@ export type VersionEntry = {
|
||||
}
|
||||
|
||||
const VERSIONS: VersionEntry[] = [
|
||||
{
|
||||
date: `2025-07-19T15:20:00-07:00`,
|
||||
product: 'web',
|
||||
body: `### Improvements
|
||||
- Removed Tumblr icon from footer as we no longer use it.
|
||||
- Reverted changes to publishing checklist since they need more work.`,
|
||||
},
|
||||
{
|
||||
date: `2025-07-16T12:40:00-07:00`,
|
||||
product: 'web',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user