mirror of
https://git.vectorsigma.ru/public/tubearchivist.git
synced 2026-08-05 01:09:30 +00:00
add membership handling endpoints
This commit is contained in:
219
frontend/src/components/MembershipAppsettings.tsx
Normal file
219
frontend/src/components/MembershipAppsettings.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import APIClient, { ApiError } from '../functions/APIClient';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
|
||||
type ApiTokenResponse = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
type ProfileUserType = {
|
||||
id: number;
|
||||
username: string;
|
||||
};
|
||||
|
||||
type SponsorTierType = {
|
||||
tier_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
max_subs: number;
|
||||
};
|
||||
|
||||
type ProfileResponseType = {
|
||||
id: number;
|
||||
user: ProfileUserType;
|
||||
sponsor_tier: SponsorTierType;
|
||||
subscription_count: number;
|
||||
subscription_is_max: boolean;
|
||||
};
|
||||
|
||||
export default function MembershipAppsettings({ show_help_text }: { show_help_text: boolean }) {
|
||||
const [inputType, setInputType] = useState('password');
|
||||
const [membershipApiToken, setMembershipApiToken] = useState<string | null>(null);
|
||||
const [newToken, setNewToken] = useState<string | null>(null);
|
||||
const [profileResponse, setProfileResponse] = useState<ProfileResponseType | null>(null);
|
||||
const [profileResponseError, setProfileResponseError] = useState('');
|
||||
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
|
||||
const [isLoadingSync, setIsLoadingSync] = useState(false);
|
||||
const [subSyncMessage, setSubSyncMessage] = useState('');
|
||||
|
||||
const fetchMembershipToken = async () => {
|
||||
const apiTokenResponse = await APIClient<ApiTokenResponse>(
|
||||
'/api/appsettings/membership/token/',
|
||||
);
|
||||
setMembershipApiToken(apiTokenResponse.data?.token || null);
|
||||
};
|
||||
|
||||
const deleteMembershipToken = async () => {
|
||||
await APIClient('/api/appsettings/membership/token/', { method: 'DELETE' });
|
||||
setMembershipApiToken(null);
|
||||
setProfileResponseError('');
|
||||
setProfileResponse(null);
|
||||
};
|
||||
|
||||
const updateToken = async () => {
|
||||
const { data } = await APIClient<ApiTokenResponse>('/api/appsettings/membership/token/', {
|
||||
method: 'POST',
|
||||
body: { token: newToken },
|
||||
});
|
||||
if (data) {
|
||||
setNewToken(null);
|
||||
setMembershipApiToken(data.token);
|
||||
setInputType('password');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMembershipToken();
|
||||
}, []);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
setProfileResponse(null);
|
||||
setProfileResponseError('');
|
||||
setSubSyncMessage('');
|
||||
|
||||
try {
|
||||
setIsLoadingProfile(true);
|
||||
const { data } = await APIClient<ProfileResponseType>('/api/appsettings/membership/profile/');
|
||||
if (data) setProfileResponse(data);
|
||||
} catch (error) {
|
||||
const apiError = error as ApiError;
|
||||
if (apiError.status && apiError.message) {
|
||||
setProfileResponseError(apiError.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingProfile(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSyncSubscriptions = async () => {
|
||||
setProfileResponseError('');
|
||||
setSubSyncMessage('');
|
||||
|
||||
try {
|
||||
setIsLoadingSync(true);
|
||||
await APIClient('/api/appsettings/membership/sync/', { method: 'POST' });
|
||||
setSubSyncMessage('Task created');
|
||||
} catch (error) {
|
||||
const apiError = error as ApiError;
|
||||
if (apiError.status && apiError.message) {
|
||||
setProfileResponseError(apiError.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingSync(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleShowKey = () => {
|
||||
if (inputType === 'password') {
|
||||
setInputType('text');
|
||||
} else {
|
||||
setInputType('password');
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
setNewToken(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Membership</h2>
|
||||
{show_help_text && (
|
||||
<div className="help-text">
|
||||
<p>
|
||||
Unlock additional perks by sponsoring this project. More details on{' '}
|
||||
<a href="https://members.tubearchivist.com/" target="_blank" rel="noopener noreferrer">
|
||||
members.tubearchivist.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Enter the API token from{' '}
|
||||
<a href="https://members.tubearchivist.com/profile">
|
||||
members.tubearchivist.com/profile
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
<li>Click on validate to verify everything is working.</li>
|
||||
<li>
|
||||
If you are subscribed to less channels than your sponsor tier allows, you can directly
|
||||
sync all your subscriptions here.
|
||||
</li>
|
||||
<ul>
|
||||
<li>Repeat the sync after changing subscriptions here.</li>
|
||||
<li>
|
||||
That will unsubscribe from channels on the membership platform if you are no longer
|
||||
subscribed here.
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="settings-box-wrapper">
|
||||
<div>
|
||||
<p>Membership API key</p>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type={inputType}
|
||||
value={newToken || membershipApiToken || ''}
|
||||
onChange={e => handleInputChange(e.target.value)}
|
||||
/>
|
||||
<div className="button-box">
|
||||
{(membershipApiToken || newToken) && (
|
||||
<button onClick={toggleShowKey}>{inputType === 'password' ? 'Show' : 'Hide'}</button>
|
||||
)}
|
||||
{newToken && (
|
||||
<>
|
||||
<button onClick={updateToken}>Save</button>
|
||||
<button onClick={() => setNewToken(null)}>Cancel</button>
|
||||
</>
|
||||
)}
|
||||
{membershipApiToken && (
|
||||
<button className="danger-button" onClick={deleteMembershipToken}>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{membershipApiToken && (
|
||||
<>
|
||||
<div>
|
||||
<p>Your Profile</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="button-box">
|
||||
{isLoadingProfile ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<button onClick={fetchProfile}>Validate</button>
|
||||
)}
|
||||
{isLoadingSync ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<button onClick={fetchSyncSubscriptions}>Sync Subscriptions</button>
|
||||
)}
|
||||
</div>
|
||||
{profileResponseError && <p className="danger-zone">Error: {profileResponseError}</p>}
|
||||
{profileResponse && (
|
||||
<>
|
||||
<p>
|
||||
Username: {profileResponse.user.username}
|
||||
<br />
|
||||
Sponsortier: {profileResponse.sponsor_tier.name} -{' '}
|
||||
{profileResponse.sponsor_tier.description}
|
||||
<br />
|
||||
Subscriptions: {profileResponse.subscription_count}/
|
||||
{profileResponse.sponsor_tier.max_subs}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{subSyncMessage && <p>Sync: {subSyncMessage}</p>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import validateCookie from '../api/actions/validateCookie';
|
||||
import deletePoToken from '../api/actions/deletePoToken';
|
||||
import updatePoToken from '../api/actions/updatePoToken';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import MembershipAppsettings from '../components/MembershipAppsettings';
|
||||
|
||||
type SettingsApplicationReponses = {
|
||||
snapshots?: SnapshotListType;
|
||||
@@ -911,6 +912,9 @@ const SettingsApplication = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-box-item">
|
||||
<MembershipAppsettings show_help_text={userConfig.show_help_text} />
|
||||
</div>
|
||||
<div className="info-box-item">
|
||||
<h2>Snapshots</h2>
|
||||
{userConfig.show_help_text && (
|
||||
|
||||
@@ -1231,6 +1231,10 @@ video:-webkit-full-screen {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.settings-box-wrapper div {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
/* settings */
|
||||
.settings-group {
|
||||
background-color: var(--highlight-bg);
|
||||
|
||||
Reference in New Issue
Block a user