mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
Compare commits
30 Commits
feat/brows
...
e09e0d9885
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e09e0d9885 | ||
| 184ea9c973 | |||
| beec80fee6 | |||
| 796313348b | |||
| 8d13c86159 | |||
| a2dfe81265 | |||
| ed3ebc9e3a | |||
| 50d92f6787 | |||
|
|
3ac4f59efd | ||
|
|
3de374392c | ||
| 61c005a585 | |||
|
|
c35e3ae1ba | ||
| 7481006dbe | |||
| d5aa3217ac | |||
| 6701090c7a | |||
| d95b935c7a | |||
| efec8602fc | |||
| 0597cb1157 | |||
| 8a924f2d52 | |||
| 79093c5057 | |||
| 2bf452c9ed | |||
| 01b2e88969 | |||
| b42b4be2d9 | |||
| 22f3cff3c1 | |||
| d01cc9f68d | |||
| 12617b3d59 | |||
| aff01be9e1 | |||
| 995a14387c | |||
| 2ce6fea782 | |||
| ab6a788b36 |
@@ -1,6 +1,6 @@
|
||||
FROM node:lts-alpine AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
ENV PATH="$PNPM_HOME/bin:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS builder
|
||||
@@ -44,4 +44,4 @@ WORKDIR /app/apps/chat
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
"@hctv/auth": "workspace:*",
|
||||
"@hctv/db": "workspace:*",
|
||||
"@hctv/hono-ws": "workspace:*",
|
||||
"@hono/node-server": "^1.14.0",
|
||||
"@hono/node-ws": "^1.1.0",
|
||||
"@leeoniya/ufuzzy": "^1.0.18",
|
||||
"@hono/node-server": "^2.0.1",
|
||||
"@hono/node-ws": "^1.3.1",
|
||||
"@leeoniya/ufuzzy": "^1.0.19",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"hono": "^4.7.5",
|
||||
"hono": "^4.12.16",
|
||||
"prom-client": "^15.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.8.2"
|
||||
"@types/node": "^25.6.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.35.2",
|
||||
"@catppuccin/starlight": "^1.0.2",
|
||||
"astro": "^5.6.1",
|
||||
"astro-mermaid": "^1.0.4",
|
||||
"mermaid": "^11.10.1",
|
||||
"sharp": "^0.34.2",
|
||||
"@astrojs/starlight": "^0.38.4",
|
||||
"@catppuccin/starlight": "^2.0.1",
|
||||
"astro": "^6.3.3",
|
||||
"astro-mermaid": "^2.0.1",
|
||||
"mermaid": "^11.14.0",
|
||||
"sharp": "^0.34.5",
|
||||
"starlight-typedoc": "^0.21.5",
|
||||
"typedoc": "^0.28.16",
|
||||
"typedoc-plugin-markdown": "^4.9.0"
|
||||
"typedoc": "^0.28.19",
|
||||
"typedoc-plugin-markdown": "^4.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0"
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
|
||||
@@ -23,10 +23,11 @@ MEDIAMTX_API_HQ=http://localhost:9997
|
||||
NEXT_PUBLIC_MEDIAMTX_INGEST_ROUTE_HQ=localhost:8890
|
||||
NEXT_PUBLIC_MEDIAMTX_WHIP_ROUTE_HQ=http://localhost:8889
|
||||
|
||||
# commented because we don't have another ingest server as of right now
|
||||
# NEXT_PUBLIC_MEDIAMTX_URL_ASIA=http://localhost:8991
|
||||
# MEDIAMTX_API_ASIA=http://localhost:9999
|
||||
# NEXT_PUBLIC_MEDIAMTX_INGEST_ROUTE_ASIA=localhost:8990
|
||||
# optional EU mirror
|
||||
# NEXT_PUBLIC_MEDIAMTX_URL_ETHANDE=https://hls-eu.example.com
|
||||
# MEDIAMTX_API_ETHANDE=https://mediamtx-api-eu.example.com
|
||||
# NEXT_PUBLIC_MEDIAMTX_INGEST_ROUTE_ETHANDE=ingest-eu.example.com:8890
|
||||
# NEXT_PUBLIC_MEDIAMTX_WHIP_ROUTE_ETHANDE=https://webrtc-eu.example.com
|
||||
|
||||
# generate with `openssl rand -base64 20`
|
||||
MEDIAMTX_PUBLISH_KEY=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM node:22-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
ENV PATH="$PNPM_HOME/bin:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"config": "tailwind.config.mts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
@@ -14,4 +14,4 @@
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ const nextConfig = {
|
||||
{
|
||||
hostname: 'eoceqrx2r7.ufs.sh'
|
||||
},
|
||||
{
|
||||
hostname: 'thesvg.org',
|
||||
}
|
||||
],
|
||||
minimumCacheTTL: 120,
|
||||
},
|
||||
|
||||
@@ -17,82 +17,83 @@
|
||||
"dependencies": {
|
||||
"@hctv/auth": "workspace:*",
|
||||
"@hctv/db": "workspace:*",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lucia-auth/adapter-prisma": "^4.0.1",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@omit/react-confirm-dialog": "^1.2.0",
|
||||
"@omit/react-confirm-dialog": "^2.0.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@scalar/api-reference-react": "^0.7.42",
|
||||
"@sentry/nextjs": "^10",
|
||||
"@slack/web-api": "^7.9.1",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@scalar/api-reference-react": "^0.9.32",
|
||||
"@sentry/nextjs": "^10.51.0",
|
||||
"@slack/web-api": "^7.15.1",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uploadthing/react": "^7.3.1",
|
||||
"ajv": "^8.17.1",
|
||||
"@uploadthing/react": "^7.3.3",
|
||||
"ajv": "^8.20.0",
|
||||
"arctic": "^3.7.0",
|
||||
"bullmq": "^5.45.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"bullmq": "^5.76.4",
|
||||
"cheerio": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "1.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"hls-video-element": "^1.5.0",
|
||||
"hls.js": "^1.6.15",
|
||||
"hls-video-element": "^1.5.11",
|
||||
"hls.js": "^1.6.16",
|
||||
"lucia": "^3.2.2",
|
||||
"lucide-react": "^0.473.0",
|
||||
"media-chrome": "^4.8.0",
|
||||
"next": "^16.1.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"node-cron": "^3.0.3",
|
||||
"nuqs": "^2.4.3",
|
||||
"pg": "^8.14.1",
|
||||
"pg-boss": "^10.1.6",
|
||||
"lucide-react": "^1.14.0",
|
||||
"media-chrome": "^4.19.0",
|
||||
"next": "^16.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"nuqs": "^2.8.9",
|
||||
"pg": "^8.20.0",
|
||||
"pg-boss": "^12.18.1",
|
||||
"prom-client": "^15.1.3",
|
||||
"react": "^19.2.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react": "^19.2.5",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-hook-form": "^7.74.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-react": "^8.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"sharp": "^0.34.3",
|
||||
"sonner": "^1.4.41",
|
||||
"swr": "^2.3.0",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"sharp": "^0.34.5",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.4.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"unified": "^11.0.5",
|
||||
"uploadthing": "^7.7.2",
|
||||
"uploadthing": "^7.7.4",
|
||||
"util-utils": "^1.0.3",
|
||||
"valtio": "^2.1.2",
|
||||
"ws": "^8.18.1",
|
||||
"zod": "^3.24.1"
|
||||
"valtio": "^2.3.2",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/ws": "^8.18.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"postcss": "^8",
|
||||
"shadcn": "^2.7.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@tailwindcss/postcss": "^4.2.4",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"postcss": "^8.5.13",
|
||||
"shadcn": "^4.6.0",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -22,11 +22,15 @@ export async function POST(request: NextRequest) {
|
||||
const parsed = schema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.error('Invalid MediaMTX auth request:', parsed.error.flatten());
|
||||
}
|
||||
|
||||
return finish('invalid request', 400, 'invalid_request');
|
||||
}
|
||||
const { action: parsedAction, protocol: parsedProtocol, path, password } = parsed.data;
|
||||
const { action: parsedAction, protocol: parsedProtocol, path, password, token } = parsed.data;
|
||||
action = parsedAction;
|
||||
protocol = parsedProtocol;
|
||||
protocol = parsedProtocol || 'none';
|
||||
|
||||
if (parsedAction === 'publish' && (parsedProtocol === 'srt' || parsedProtocol === 'webrtc')) {
|
||||
const channelKey = await redis.get(`streamKey:${path}`);
|
||||
@@ -81,7 +85,7 @@ export async function POST(request: NextRequest) {
|
||||
return finish('authorized', 200, 'authorized_read');
|
||||
}
|
||||
if (parsedAction === 'api') {
|
||||
if (password === process.env.MEDIAMTX_API_KEY) {
|
||||
if (password === process.env.MEDIAMTX_API_KEY || token === process.env.MEDIAMTX_API_KEY) {
|
||||
return finish('authorized api', 200, 'authorized_api');
|
||||
}
|
||||
|
||||
@@ -91,14 +95,22 @@ export async function POST(request: NextRequest) {
|
||||
return finish('uhh', 401, 'unauthorized');
|
||||
}
|
||||
|
||||
const emptyableString = z
|
||||
.string()
|
||||
.nullish()
|
||||
.transform((value) => value ?? '');
|
||||
|
||||
const schema = z.object({
|
||||
user: z.string(),
|
||||
password: z.string(),
|
||||
token: z.string(),
|
||||
ip: z.string(),
|
||||
user: emptyableString,
|
||||
password: emptyableString,
|
||||
token: emptyableString,
|
||||
ip: emptyableString,
|
||||
action: z.enum(['publish', 'read', 'playback', 'api', 'metrics', 'pprof']),
|
||||
path: z.string(),
|
||||
protocol: z.enum(['rtsp', 'rtmp', 'hls', 'webrtc', 'srt']),
|
||||
id: z.string().nullable(),
|
||||
query: z.string(),
|
||||
path: emptyableString,
|
||||
protocol: z
|
||||
.union([z.enum(['rtsp', 'rtmp', 'hls', 'webrtc', 'srt']), z.literal('')])
|
||||
.nullish()
|
||||
.transform((value) => value ?? ''),
|
||||
id: z.string().nullable().default(null),
|
||||
query: emptyableString,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,13 @@ export async function POST(request: NextRequest, segmentData: { params: Params }
|
||||
const body = await request.json();
|
||||
const parsedBody = bodySchema.safeParse(body);
|
||||
if (!parsedBody.success) {
|
||||
return new Response(JSON.stringify({ success: false, error: parsedBody.error.errors.map(e => e.message).join(', ') }), { status: 400 });
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: parsedBody.error.issues.map((issue) => issue.message).join(', '),
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { action, name } = parsedBody.data;
|
||||
@@ -121,4 +127,4 @@ export async function GET(request: NextRequest, segmentData: { params: Params })
|
||||
function generateApiKey() {
|
||||
const uuid = crypto.randomUUID().replace(/-/g, '');
|
||||
return `hctvb_${uuid}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { getMediamtxClientEnvs } from '@/lib/utils/mediamtx/client';
|
||||
import {
|
||||
getMediamtxClientEnvs,
|
||||
getMediamtxClientRegionOptions,
|
||||
} from '@/lib/utils/mediamtx/client';
|
||||
import type { MediaMTXRegion } from '@/lib/utils/mediamtx/regions';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
@@ -121,6 +124,7 @@ export default function ChannelSettingsClient({
|
||||
const [selTab, setSelTab] = useQueryState('tab', parseAsString.withDefault('general'));
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const serverOptions = getMediamtxClientRegionOptions();
|
||||
const [region, setRegion] = useState<MediaMTXRegion>('hq');
|
||||
const channelList = useOwnedChannels();
|
||||
const {
|
||||
@@ -587,7 +591,11 @@ export default function ChannelSettingsClient({
|
||||
<SelectValue placeholder="Select region" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hq">HQ Server A 🇺🇸</SelectItem>
|
||||
{serverOptions.map((server) => (
|
||||
<SelectItem key={server.value} value={server.value}>
|
||||
{server.label} {server.emoji}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -1110,9 +1118,7 @@ export default function ChannelSettingsClient({
|
||||
value={
|
||||
typeof field.value === 'string'
|
||||
? field.value
|
||||
: Array.isArray(field.value)
|
||||
? field.value.join('\n')
|
||||
: ''
|
||||
: ''
|
||||
}
|
||||
disabled={channel.is247}
|
||||
rows={4}
|
||||
|
||||
@@ -34,10 +34,10 @@ import type { MediaMTXRegion } from '@/lib/utils/mediamtx/regions';
|
||||
|
||||
export default function Page() {
|
||||
const serverOptions = getMediamtxClientRegionOptions();
|
||||
const whipEnabledServerOptions = serverOptions.filter((server) => server.whipEnabled);
|
||||
const defaultRegion = whipEnabledServerOptions[0]?.value ?? 'hq';
|
||||
const [selectedChannel, setSelectedChannel] = useState('');
|
||||
const [selectedRegion, setSelectedRegion] = useState<MediaMTXRegion>(
|
||||
serverOptions[0]?.value ?? 'hq'
|
||||
);
|
||||
const [selectedRegion, setSelectedRegion] = useState<MediaMTXRegion>(defaultRegion);
|
||||
const { channels, isLoading: isLoadingChannels } = useOwnedChannels();
|
||||
const ownedChannels = channels.map(({ channel }) => channel);
|
||||
const {
|
||||
@@ -67,12 +67,14 @@ export default function Page() {
|
||||
});
|
||||
|
||||
const hasChannels = ownedChannels.length > 0;
|
||||
const hasServerOptions = serverOptions.length > 0;
|
||||
const selectedServer = whipEnabledServerOptions.find((server) => server.value === selectedRegion);
|
||||
const hasWhipEnabledServerOptions = whipEnabledServerOptions.length > 0;
|
||||
const canStartPublishing =
|
||||
!isSessionActive &&
|
||||
!isPreviewingSource &&
|
||||
Boolean(selectedChannel) &&
|
||||
Boolean(streamKey) &&
|
||||
Boolean(selectedServer?.whipEnabled) &&
|
||||
!isLoadingStreamKey;
|
||||
const channelPlaceholder = isLoadingChannels ? 'Loading channels...' : 'Select a channel';
|
||||
const primaryIssue = issue ?? browserWarning;
|
||||
@@ -87,6 +89,23 @@ export default function Page() {
|
||||
}
|
||||
}, [isSessionActive, ownedChannels, selectedChannel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [isLive]);
|
||||
|
||||
const statusLabel = isLive
|
||||
? 'LIVE'
|
||||
: isSwitchingSource
|
||||
@@ -252,18 +271,14 @@ export default function Page() {
|
||||
<Select
|
||||
value={selectedRegion}
|
||||
onValueChange={(value) => setSelectedRegion(value as MediaMTXRegion)}
|
||||
disabled={isSessionActive || !hasServerOptions}
|
||||
disabled={isSessionActive || !hasWhipEnabledServerOptions}
|
||||
>
|
||||
<SelectTrigger className="w-44">
|
||||
<SelectValue placeholder="Select server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serverOptions.map((server) => (
|
||||
<SelectItem
|
||||
key={server.value}
|
||||
value={server.value}
|
||||
disabled={!server.whipEnabled}
|
||||
>
|
||||
{whipEnabledServerOptions.map((server) => (
|
||||
<SelectItem key={server.value} value={server.value}>
|
||||
{server.label} {server.emoji}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { cookies } from 'next/headers';
|
||||
import '../globals.css';
|
||||
import Navbar from '@/components/app/NavBar/NavBar';
|
||||
@@ -9,7 +8,6 @@ import { Toaster } from '@/components/ui/sonner';
|
||||
import { ThemeProvider } from '@/lib/providers/ThemeProvider';
|
||||
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||
import Sidebar from '@/components/app/Sidebar/Sidebar';
|
||||
import { cn } from '@/lib/utils';
|
||||
import EditLivestream from '@/components/app/EditLivestream/EditLivestream';
|
||||
import { StreamInfoProvider } from '@/lib/providers/StreamInfoProvider';
|
||||
import { NextSSRPlugin } from "@uploadthing/react/next-ssr-plugin";
|
||||
@@ -19,8 +17,6 @@ import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
||||
import SonnerNewVersion from '@/components/app/SonnerNewVersion/SonnerNewVersion';
|
||||
import ConfirmDialogProvider from '@/lib/providers/ConfirmProvider';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'hackclub.tv',
|
||||
description: "Hack Club's livestreaming platform",
|
||||
@@ -37,7 +33,7 @@ export default async function RootLayout({
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={cn('flex flex-col h-screen', inter.className)}>
|
||||
<body className="flex h-screen flex-col">
|
||||
<SessionProvider value={sessionData}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
|
||||
@@ -1,128 +1,163 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Light theme - based on your color scheme */
|
||||
@config "../../tailwind.config.mts";
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* Main background and foreground */
|
||||
--background: 350 59% 98%; /* FDF7F8 - main background */
|
||||
--foreground: 351 34% 30%; /* 5D3A3F - main text */
|
||||
@font-face {
|
||||
font-family: 'Phantom Sans';
|
||||
src:
|
||||
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Regular.woff2') format('woff2'),
|
||||
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Regular.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Muted elements */
|
||||
--muted: 350 40% 93%; /* F8E8EA - muted background */
|
||||
--muted-foreground: 350 30% 45%; /* Lighter version of main text */
|
||||
@font-face {
|
||||
font-family: 'Phantom Sans';
|
||||
src:
|
||||
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Italic.woff2') format('woff2'),
|
||||
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Italic.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Popover and card */
|
||||
--popover: 0 0% 100%; /* FFFFFF - popover background */
|
||||
--popover-foreground: 351 34% 30%; /* 5D3A3F - popover text */
|
||||
--card: 0 0% 100%; /* FFFFFF - card background */
|
||||
--card-foreground: 351 34% 30%; /* 5D3A3F - card text */
|
||||
@font-face {
|
||||
font-family: 'Phantom Sans';
|
||||
src:
|
||||
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Bold.woff2') format('woff2'),
|
||||
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Bold.woff') format('woff');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Border and input */
|
||||
--border: 350 30% 85%; /* Derived border color */
|
||||
--input: 350 30% 85%; /* Input background */
|
||||
:root {
|
||||
--background: hsl(350 59% 98%);
|
||||
--foreground: hsl(351 34% 30%);
|
||||
--muted: hsl(350 40% 93%);
|
||||
--muted-foreground: hsl(350 30% 45%);
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(351 34% 30%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(351 34% 30%);
|
||||
--border: hsl(350 30% 85%);
|
||||
--input: hsl(350 30% 85%);
|
||||
--primary: hsl(350 70% 50%);
|
||||
--primary-foreground: hsl(0 0% 100%);
|
||||
--secondary: hsl(350 40% 93%);
|
||||
--secondary-foreground: hsl(351 34% 30%);
|
||||
--accent: hsl(350 70% 40%);
|
||||
--accent-foreground: hsl(0 0% 100%);
|
||||
--destructive: hsl(350 70% 55%);
|
||||
--destructive-foreground: hsl(0 0% 100%);
|
||||
--ring: hsl(350 70% 50%);
|
||||
--surface-1: hsl(350 40% 93%);
|
||||
--surface-2: hsl(350 35% 88%);
|
||||
--mantle: hsl(350 59% 98%);
|
||||
--mantle-foreground: hsl(351 34% 30%);
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: hsl(350 59% 98%);
|
||||
--sidebar-foreground: hsl(351 34% 30%);
|
||||
--sidebar-primary: hsl(350 70% 50%);
|
||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||
--sidebar-accent: hsl(350 40% 93%);
|
||||
--sidebar-accent-foreground: hsl(351 34% 30%);
|
||||
--sidebar-border: hsl(350 30% 85%);
|
||||
--sidebar-ring: hsl(350 70% 50%);
|
||||
}
|
||||
|
||||
/* Primary actions */
|
||||
--primary: 350 70% 50%; /* C8394F - primary button */
|
||||
--primary-foreground: 0 0% 100%; /* FFFFFF - text on primary */
|
||||
.dark {
|
||||
--background: hsl(350 20% 15%);
|
||||
--foreground: hsl(350 30% 92%);
|
||||
--muted: hsl(350 20% 25%);
|
||||
--muted-foreground: hsl(350 30% 75%);
|
||||
--popover: hsl(350 20% 15%);
|
||||
--popover-foreground: hsl(350 30% 92%);
|
||||
--card: hsl(350 20% 15%);
|
||||
--card-foreground: hsl(350 30% 92%);
|
||||
--border: hsl(350 20% 35%);
|
||||
--input: hsl(350 20% 35%);
|
||||
--primary: hsl(350 100% 75%);
|
||||
--primary-foreground: hsl(350 20% 15%);
|
||||
--secondary: hsl(350 20% 25%);
|
||||
--secondary-foreground: hsl(350 30% 92%);
|
||||
--accent: hsl(350 100% 80%);
|
||||
--accent-foreground: hsl(350 20% 15%);
|
||||
--destructive: hsl(350 100% 70%);
|
||||
--destructive-foreground: hsl(350 20% 15%);
|
||||
--ring: hsl(350 100% 75%);
|
||||
--surface-1: hsl(350 20% 25%);
|
||||
--surface-2: hsl(350 20% 35%);
|
||||
--mantle: hsl(350 20% 12%);
|
||||
--mantle-foreground: hsl(350 30% 92%);
|
||||
--sidebar-background: hsl(350 20% 12%);
|
||||
--sidebar-foreground: hsl(350 30% 92%);
|
||||
--sidebar-primary: hsl(350 100% 75%);
|
||||
--sidebar-primary-foreground: hsl(350 20% 15%);
|
||||
--sidebar-accent: hsl(350 20% 25%);
|
||||
--sidebar-accent-foreground: hsl(350 30% 92%);
|
||||
--sidebar-border: hsl(350 20% 35%);
|
||||
--sidebar-ring: hsl(350 100% 75%);
|
||||
}
|
||||
|
||||
/* Secondary elements */
|
||||
--secondary: 350 40% 93%; /* F8E8EA - secondary background */
|
||||
--secondary-foreground: 351 34% 30%; /* 5D3A3F - text on secondary */
|
||||
@theme inline {
|
||||
--font-sans: 'Phantom Sans', ui-sans-serif, system-ui, sans-serif;
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-ring: var(--ring);
|
||||
--color-surface1: var(--surface-1);
|
||||
--color-surface2: var(--surface-2);
|
||||
--color-mantle: var(--mantle);
|
||||
--color-mantle-foreground: var(--mantle-foreground);
|
||||
--color-sidebar: var(--sidebar-background);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Accent elements */
|
||||
--accent: 350 70% 40%; /* A12D3E - accent color */
|
||||
--accent-foreground: 0 0% 100%; /* FFFFFF - text on accent */
|
||||
|
||||
/* Destructive actions */
|
||||
--destructive: 350 70% 55%; /* D63C56 - error/destroy */
|
||||
--destructive-foreground: 0 0% 100%; /* FFFFFF - text on destructive */
|
||||
|
||||
/* Focus ring */
|
||||
--ring: 350 70% 50%; /* C8394F - focus ring */
|
||||
|
||||
/* Surface colors */
|
||||
--surface-1: 350 40% 93%; /* F8E8EA - surface 1 */
|
||||
--surface-2: 350 35% 88%; /* Derived surface 2 */
|
||||
|
||||
/* Mantle */
|
||||
--mantle: 350 59% 98%; /* FDF7F8 - mantle */
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Sidebar specific */
|
||||
--sidebar-background: 350 59% 98%; /* FDF7F8 - sidebar bg */
|
||||
--sidebar-foreground: 351 34% 30%; /* 5D3A3F - sidebar text */
|
||||
--sidebar-primary: 350 70% 50%; /* C8394F - sidebar primary */
|
||||
--sidebar-primary-foreground: 0 0% 100%; /* FFFFFF - text on sidebar primary */
|
||||
--sidebar-accent: 350 40% 93%; /* F8E8EA - sidebar accent */
|
||||
--sidebar-accent-foreground: 351 34% 30%; /* 5D3A3F - text on sidebar accent */
|
||||
--sidebar-border: 350 30% 85%; /* Derived border */
|
||||
--sidebar-ring: 350 70% 50%; /* C8394F - sidebar focus ring */
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Dark theme - based on your color scheme */
|
||||
|
||||
/* Main background and foreground */
|
||||
--background: 350 20% 15%; /* 2A1F21 - main background */
|
||||
--foreground: 350 30% 92%; /* F5E6E8 - main text */
|
||||
|
||||
/* Muted elements */
|
||||
--muted: 350 20% 25%; /* 4A2D31 - muted background */
|
||||
--muted-foreground: 350 30% 75%; /* Lighter version of main text */
|
||||
|
||||
/* Popover and card */
|
||||
--popover: 350 20% 15%; /* 2A1F21 - popover background */
|
||||
--popover-foreground: 350 30% 92%; /* F5E6E8 - popover text */
|
||||
--card: 350 20% 15%; /* 2A1F21 - card background */
|
||||
--card-foreground: 350 30% 92%; /* F5E6E8 - card text */
|
||||
|
||||
/* Border and input */
|
||||
--border: 350 20% 35%; /* Derived border color */
|
||||
--input: 350 20% 35%; /* Input background */
|
||||
|
||||
/* Primary actions */
|
||||
--primary: 350 100% 75%; /* FF7A8A - primary button */
|
||||
--primary-foreground: 350 20% 15%; /* 2A1F21 - text on primary */
|
||||
|
||||
/* Secondary elements */
|
||||
--secondary: 350 20% 25%; /* 4A2D31 - secondary background */
|
||||
--secondary-foreground: 350 30% 92%; /* F5E6E8 - text on secondary */
|
||||
|
||||
/* Accent elements */
|
||||
--accent: 350 100% 80%; /* FF9AAA - accent color */
|
||||
--accent-foreground: 350 20% 15%; /* 2A1F21 - text on accent */
|
||||
|
||||
/* Destructive actions */
|
||||
--destructive: 350 100% 70%; /* FF6B7D - error/destroy */
|
||||
--destructive-foreground: 350 20% 15%; /* 2A1F21 - text on destructive */
|
||||
|
||||
/* Focus ring */
|
||||
--ring: 350 100% 75%; /* FF7A8A - focus ring */
|
||||
|
||||
/* Surface colors */
|
||||
--surface-1: 350 20% 25%; /* 4A2D31 - surface 1 */
|
||||
--surface-2: 350 20% 35%; /* Derived surface 2 */
|
||||
|
||||
/* Mantle */
|
||||
--mantle: 350 20% 12%; /* 1F1617 - mantle */
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Sidebar specific */
|
||||
--sidebar-background: 350 20% 12%; /* 1F1617 - sidebar bg */
|
||||
--sidebar-foreground: 350 30% 92%; /* F5E6E8 - sidebar text */
|
||||
--sidebar-primary: 350 100% 75%; /* FF7A8A - sidebar primary */
|
||||
--sidebar-primary-foreground: 350 20% 15%; /* 2A1F21 - text on sidebar primary */
|
||||
--sidebar-accent: 350 20% 25%; /* 4A2D31 - sidebar accent */
|
||||
--sidebar-accent-foreground: 350 30% 92%; /* F5E6E8 - text on sidebar accent */
|
||||
--sidebar-border: 350 20% 35%; /* Derived border */
|
||||
--sidebar-ring: 350 100% 75%; /* FF7A8A - sidebar focus ring */
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,31 +166,30 @@
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
font-family: 'Phantom Sans', ui-sans-serif, system-ui, sans-serif;
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||||
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply scroll-m-20 pb-2 text-3xl font-semibold tracking-tight first:mt-0;
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
media-controller {
|
||||
--media-primary-color: #ffffff;
|
||||
--media-secondary-color: transparent;
|
||||
--media-control-background: transparent;
|
||||
--media-control-hover-background: hsla(var(--primary), 0.4);
|
||||
--media-control-hover-background: color-mix(in oklab, var(--primary) 40%, transparent);
|
||||
|
||||
/* Range colors */
|
||||
--media-range-track-background: hsla(0, 0%, 100%, 0.3);
|
||||
--media-range-bar-color: hsl(var(--primary));
|
||||
--media-range-thumb-background: hsl(var(--primary));
|
||||
--media-range-bar-color: var(--primary);
|
||||
--media-range-thumb-background: var(--primary);
|
||||
--media-range-thumb-border-radius: 50%;
|
||||
--media-range-thumb-height: 12px;
|
||||
--media-range-thumb-width: 12px;
|
||||
@@ -164,7 +198,7 @@ media-controller {
|
||||
/* Layout & structure */
|
||||
border-radius: calc(var(--radius) * 1.5);
|
||||
overflow: hidden;
|
||||
border: 1px solid hsla(var(--border), 0.2);
|
||||
border: 1px solid color-mix(in oklab, var(--border) 20%, transparent);
|
||||
background-color: #000;
|
||||
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.3);
|
||||
}
|
||||
@@ -182,9 +216,9 @@ media-control-bar {
|
||||
}
|
||||
|
||||
media-time-range {
|
||||
--media-preview-background: hsla(var(--card), 0.95);
|
||||
--media-preview-background: color-mix(in oklab, var(--card) 95%, transparent);
|
||||
--media-preview-border-radius: var(--radius);
|
||||
--media-time-display-color: hsl(var(--foreground));
|
||||
--media-time-display-color: var(--foreground);
|
||||
}
|
||||
|
||||
media-time-display {
|
||||
@@ -207,8 +241,8 @@ media-controller:not([mediapaused])[userinactive]::part(centered-layer) {
|
||||
media-loading-indicator {
|
||||
--media-loading-icon-width: 56px;
|
||||
--media-loading-icon-height: 56px;
|
||||
--media-loading-icon-color: hsl(var(--primary));
|
||||
filter: drop-shadow(0 0 8px hsla(var(--primary), 0.4));
|
||||
--media-loading-icon-color: var(--primary);
|
||||
filter: drop-shadow(0 0 8px color-mix(in oklab, var(--primary) 40%, transparent));
|
||||
}
|
||||
|
||||
media-play-button,
|
||||
@@ -225,7 +259,7 @@ media-mute-button:hover,
|
||||
media-fullscreen-button:hover,
|
||||
media-chrome-button:hover {
|
||||
transform: scale(1.1);
|
||||
--media-control-background: hsla(var(--primary), 0.85);
|
||||
--media-control-background: color-mix(in oklab, var(--primary) 85%, transparent);
|
||||
--media-button-icon-color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -241,4 +275,4 @@ media-chrome-button:hover {
|
||||
media-volume-range {
|
||||
width: 90px;
|
||||
height: 40px; /* Aligns with standard media button heights */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ export default function ChatPanel(props: Props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${props.isObsPanel ? 'w-full text-white' : 'md:border-l border-border bg-mantle w-[350px] max-w-[350px]'} flex flex-col h-full`}
|
||||
className={`${props.isObsPanel ? 'w-full text-white' : 'w-full max-w-none md:w-[350px] md:max-w-[350px] md:border-l border-border bg-mantle'} flex flex-col h-full min-w-0`}
|
||||
>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
|
||||
@@ -9,7 +9,7 @@ interface EmojiSearchProps {
|
||||
onSelect: (emoji: string) => void;
|
||||
socket: WebSocket | null;
|
||||
emojiMap: Map<string, string>;
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
}
|
||||
|
||||
export function EmojiSearch({
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function LiveStream(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${isMobile ? 'flex flex-col' : 'flex'} h-[calc(100vh-64px)] w-full`}>
|
||||
<div className={`${isMobile ? 'flex flex-col' : 'flex'} h-[calc(100vh-64px)] w-full min-w-0`}>
|
||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
{isRestricted && props.canViewRestrictedStream && (
|
||||
<div className="flex items-start gap-3 border-b border-amber-500/30 bg-amber-500/10 px-4 py-3 text-foreground">
|
||||
@@ -80,9 +80,11 @@ export default function LiveStream(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<StreamPlayer />
|
||||
<div className="min-h-0 flex-1 overflow-hidden bg-black">
|
||||
<StreamPlayer />
|
||||
</div>
|
||||
{isMobile && (
|
||||
<div className="flex-1 min-h-[250px] max-h-[400px] border-t border-border">
|
||||
<div className="w-full min-w-0 flex-1 min-h-[250px] max-h-[400px] border-t border-border">
|
||||
<ChatPanel />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
PenSquare,
|
||||
LogOut,
|
||||
Code,
|
||||
Github,
|
||||
Heart,
|
||||
Radio,
|
||||
} from 'lucide-react';
|
||||
@@ -31,6 +30,7 @@ import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import Image from 'next/image';
|
||||
import Logo from '@/lib/assets/logo.webp';
|
||||
import { usePersonalChannels } from '@/lib/hooks/useUserList';
|
||||
import { JSX } from 'react';
|
||||
|
||||
export default function Navbar(props: Props) {
|
||||
const { user } = useSession();
|
||||
@@ -140,8 +140,13 @@ export default function Navbar(props: Props) {
|
||||
</Link>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<Link href={'https://github.com/SrIzan10/hctv'} target="_blank" rel="noreferrer">
|
||||
<DropdownMenuItem className={`${menuItemClass} justify-center text-xs`}>
|
||||
<Github className="w-3.5 h-3.5 mr-1.5" />
|
||||
<DropdownMenuItem className={`${menuItemClass} justify-center text-xs gap-2`}>
|
||||
<Image
|
||||
src="https://thesvg.org/icons/github/dark.svg"
|
||||
alt="GitHub"
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
Github
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
|
||||
@@ -177,8 +177,8 @@ export default function StreamPlayer() {
|
||||
}, [clearWaitingTimeout, playerKey, triggerRecovery]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<MediaController className="w-full aspect-video">
|
||||
<div className="relative flex h-full w-full min-w-0 items-center justify-center bg-black">
|
||||
<MediaController className="h-full w-full">
|
||||
<HlsVideo
|
||||
key={playerKey}
|
||||
ref={videoRef}
|
||||
@@ -186,6 +186,7 @@ export default function StreamPlayer() {
|
||||
crossOrigin="anonymous"
|
||||
playsInline
|
||||
autoplay
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
<MediaLoadingIndicator slot="centered-chrome" noAutohide />
|
||||
<MediaControlBar className="w-full px-2 sm:px-4 pb-1">
|
||||
@@ -200,7 +201,10 @@ export default function StreamPlayer() {
|
||||
{(process.env.NODE_ENV === 'development' || userInfo?.isLive) && (
|
||||
<MediaChromeButton onClick={() => triggerRecovery('manual_reload')}>
|
||||
<span className="flex h-4 w-4 items-center justify-center">
|
||||
<RefreshCw className={cn("h-5 w-5 shrink-0", isRecovering && "animate-spin")} strokeWidth={2.5} />
|
||||
<RefreshCw
|
||||
className={cn('h-5 w-5 shrink-0', isRecovering && 'animate-spin')}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</span>
|
||||
<span slot="tooltip-content">Retry stream</span>
|
||||
</MediaChromeButton>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Path, useForm } from 'react-hook-form';
|
||||
import { FieldValues, Path, useForm } from 'react-hook-form';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { z } from 'zod';
|
||||
import type { UniversalFormProps } from './types';
|
||||
import SubmitButton from '../SubmitButton/SubmitButton';
|
||||
import { useActionState } from 'react';
|
||||
@@ -43,7 +42,7 @@ export const schemaDb = [
|
||||
{ name: 'updateNotificationChannels', zod: updateNotificationChannelsSchema },
|
||||
] as const;
|
||||
|
||||
export function UniversalForm<T extends z.ZodType>({
|
||||
export function UniversalForm({
|
||||
fields,
|
||||
schemaName,
|
||||
action,
|
||||
@@ -54,7 +53,7 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
submitClassname,
|
||||
otherSubmitButton,
|
||||
submitButtonDivClassname,
|
||||
}: UniversalFormProps<T>) {
|
||||
}: UniversalFormProps) {
|
||||
// @ts-expect-error - idk
|
||||
const [state, formAction] = useActionState<{ success: boolean; error?: string }>(action, null);
|
||||
const schema = schemaDb.find((s) => s.name === schemaName)?.zod;
|
||||
@@ -72,11 +71,9 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
return { ...values, ...defaultValues };
|
||||
}, [fields, defaultValues]);
|
||||
|
||||
type FormData = z.infer<T>;
|
||||
|
||||
const form = useForm<FormData>({
|
||||
const form = useForm<FieldValues>({
|
||||
resolver: zodResolver(schema as any),
|
||||
defaultValues: initialValues as FormData,
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -95,7 +92,7 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
<FormField
|
||||
key={field.name}
|
||||
control={form.control}
|
||||
name={field.name as Path<FormData>}
|
||||
name={field.name as Path<FieldValues>}
|
||||
render={({ field: formField }) => (
|
||||
<FormItem className={field.type === 'hidden' ? 'hidden' : undefined}>
|
||||
{field.type !== 'hidden' && field.label && <FormLabel>{field.label}</FormLabel>}
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import type { HTMLInputTypeAttribute, Ref } from 'react';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { ControllerRenderProps, FieldValues } from 'react-hook-form';
|
||||
import { schemaDb } from './UniversalForm';
|
||||
|
||||
export type FormFieldConfig<T extends z.ZodType> = {
|
||||
type FormFieldValue = string | number | boolean | null | undefined;
|
||||
|
||||
export type FormFieldConfig = {
|
||||
name: string;
|
||||
label?: string;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
value?: z.input<T>[keyof z.input<T>];
|
||||
value?: FormFieldValue;
|
||||
textArea?: boolean;
|
||||
textAreaRows?: number;
|
||||
maxChars?: number;
|
||||
inputFilter?: RegExp;
|
||||
component?: (
|
||||
props: {
|
||||
field: ControllerRenderProps<z.infer<T>>;
|
||||
field: ControllerRenderProps<FieldValues>;
|
||||
} & Record<string, unknown>
|
||||
) => React.ReactNode;
|
||||
componentProps?: Record<string, any>;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
export type UniversalFormProps<T extends z.ZodType> = {
|
||||
fields: FormFieldConfig<T>[];
|
||||
export type UniversalFormProps = {
|
||||
fields: FormFieldConfig[];
|
||||
schemaName: (typeof schemaDb)[number]['name'];
|
||||
action: (prev: any, formData: FormData) => void;
|
||||
onActionComplete?: (result: any) => void;
|
||||
defaultValues?: Partial<z.infer<T>>;
|
||||
defaultValues?: Partial<FieldValues>;
|
||||
formRef?: Ref<HTMLFormElement>;
|
||||
submitText?: string;
|
||||
submitClassname?: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Preview } from '@/components/ui/channel-desc-fancy-area/preview';
|
||||
|
||||
export default function UserInfoCard(props: Props) {
|
||||
return (
|
||||
<div className="bg-mantle p-4 border-b h-48 flex flex-col">
|
||||
<div className="bg-mantle p-4 border-b h-48 shrink-0 flex flex-col">
|
||||
<div className="flex items-start justify-between mb-4 flex-shrink-0">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
|
||||
@@ -26,7 +26,6 @@ export function useProcessor(md: string) {
|
||||
mention: ["handle"],
|
||||
},
|
||||
})
|
||||
// @ts-expect-error because mention is not valid html-tag
|
||||
.use(rehypeReact, {
|
||||
createElement,
|
||||
Fragment,
|
||||
@@ -37,7 +36,7 @@ export function useProcessor(md: string) {
|
||||
},
|
||||
})
|
||||
.process(text)
|
||||
.then((file) => {
|
||||
.then((file: { result: React.ReactNode }) => {
|
||||
setContent(file.result);
|
||||
});
|
||||
}, [text]);
|
||||
|
||||
@@ -141,7 +141,7 @@ const SidebarProvider = React.forwardRef<
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh has-[[data-variant=inset]]:bg-sidebar",
|
||||
"group/sidebar-wrapper flex min-h-svh flex-col has-[[data-variant=inset]]:bg-sidebar",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -181,7 +181,7 @@ const Sidebar = React.forwardRef<
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
"flex h-full w-[var(--sidebar-width)] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -198,7 +198,7 @@ const Sidebar = React.forwardRef<
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
className="w-[var(--sidebar-width)] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
@@ -224,24 +224,24 @@ const Sidebar = React.forwardRef<
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
"duration-200 relative h-[calc(100vh-4rem)] w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
|
||||
"duration-200 relative h-[calc(100vh-4rem)] w-[var(--sidebar-width)] transition-[left,right,width] ease-linear md:flex",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--spacing)*4)]"
|
||||
: "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
|
||||
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)] transition-[left,right,width] ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--spacing)*4+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -323,7 +323,7 @@ const SidebarInset = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex min-h-svh flex-1 flex-col bg-background",
|
||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
"peer-data-[variant=inset]:min-h-[calc(100svh-var(--spacing)*4)] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -518,7 +518,7 @@ const sidebarMenuButtonVariants = cva(
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
@@ -669,7 +669,7 @@ const SidebarMenuSkeleton = React.forwardRef<
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 flex-1 max-w-[--skeleton-width]"
|
||||
className="h-4 flex-1 max-w-[var(--skeleton-width)]"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
|
||||
@@ -90,6 +90,11 @@ export async function syncStream() {
|
||||
|
||||
for (const r of regions) {
|
||||
const region = MEDIAMTX_SERVER_REGIONS[r];
|
||||
if (!region) {
|
||||
// continuing bc of the next if check
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!region.apiAuthHeader) {
|
||||
throw new Error('MEDIAMTX_API_KEY is required when querying the MediaMTX API');
|
||||
}
|
||||
|
||||
@@ -54,5 +54,11 @@ export function getMediamtxClientRegionOptions(): MediaMTXClientRegionOption[] {
|
||||
label: 'HQ Server A',
|
||||
whipEnabled: false,
|
||||
},
|
||||
{
|
||||
value: 'ethande',
|
||||
emoji: '🇩🇪',
|
||||
label: 'eth0\'s VPS',
|
||||
whipEnabled: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@ export interface MediaMTXEnvs {
|
||||
apiAuthHeader?: string;
|
||||
}
|
||||
|
||||
export const MEDIAMTX_SERVER_REGIONS: Record<MediaMTXRegion, MediaMTXEnvs> = {
|
||||
hq: {
|
||||
apiUrl: process.env.MEDIAMTX_API_HQ!,
|
||||
apiAuthHeader: getMediamtxApiAuthHeader(),
|
||||
},
|
||||
export const MEDIAMTX_SERVER_REGIONS: Partial<Record<MediaMTXRegion, MediaMTXEnvs>> = {
|
||||
hq: createMediamtxEnvs(process.env.MEDIAMTX_API_HQ),
|
||||
ethande: createMediamtxEnvs(process.env.MEDIAMTX_API_ETHANDE),
|
||||
};
|
||||
|
||||
export function getMediamtxEnvs(region: MediaMTXRegion = 'hq'): MediaMTXEnvs {
|
||||
@@ -31,3 +29,14 @@ function getMediamtxApiAuthHeader() {
|
||||
|
||||
return `Basic ${Buffer.from(`hctv-api:${apiKey}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
function createMediamtxEnvs(apiUrl?: string): MediaMTXEnvs | undefined {
|
||||
if (!apiUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
apiUrl,
|
||||
apiAuthHeader: getMediamtxApiAuthHeader(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,8 +30,11 @@ export default async function zodVerify<TSchema extends z.ZodTypeAny>(
|
||||
|
||||
const zod = schema.safeParse(obj);
|
||||
if (!zod.success) {
|
||||
const [issue] = zod.error.issues;
|
||||
const path = issue.path[0] === undefined ? 'form' : String(issue.path[0]);
|
||||
|
||||
return {
|
||||
error: `From ${zod.error.errors[0].path[0]}: ${zod.error.errors[0].message}`,
|
||||
error: `From ${path}: ${issue.message}`,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,109 +1,20 @@
|
||||
import type { Config } from "tailwindcss"
|
||||
import { uploadthingPlugin } from 'uploadthing/tw'
|
||||
import * as tan from 'tailwindcss-animate'
|
||||
import { uploadthingPlugin } from 'uploadthing/tw';
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
darkMode: 'class',
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
prefix: '',
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px'
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
surface1: {
|
||||
DEFAULT: 'hsl(var(--surface-1))'
|
||||
},
|
||||
surface2: {
|
||||
DEFAULT: 'hsl(var(--surface-2))'
|
||||
},
|
||||
mantle: {
|
||||
DEFAULT: 'hsl(var(--mantle))'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0'
|
||||
},
|
||||
to: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
}
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
},
|
||||
to: {
|
||||
height: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
}
|
||||
}
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [tan, uploadthingPlugin],
|
||||
} satisfies Config
|
||||
plugins: [uploadthingPlugin],
|
||||
} satisfies Config;
|
||||
|
||||
export default config
|
||||
export default config;
|
||||
|
||||
@@ -61,6 +61,10 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/mediamtx/Dockerfile
|
||||
environment:
|
||||
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
|
||||
MTX_AUTHHTTPADDRESS: ${MEDIAMTX_AUTH_HTTP_ADDRESS:-http://hctv:3000/api/mediamtx/publish}
|
||||
MTX_WEBRTCADDITIONALHOSTS: ${MEDIAMTX_WEBRTC_ADDITIONAL_HOSTS:-}
|
||||
ports:
|
||||
- '8890:8890/udp'
|
||||
postgres-exporter:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
FROM bluenviron/mediamtx:1 AS mediamtx
|
||||
|
||||
FROM ubuntu:24.04
|
||||
FROM alpine:3.21
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
COPY --from=mediamtx /mediamtx /
|
||||
COPY ./docker/mediamtx/mediamtx.yml /mediamtx.yml
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
ACME_EMAIL=ops@hackclub.tv
|
||||
CF_DNS_API_TOKEN=cloudflare_dns_edit_token
|
||||
|
||||
# public hostnames and stuff
|
||||
MEDIAMTX_HLS_HOST=hls.hackclub.tv
|
||||
@@ -8,5 +9,5 @@ MEDIAMTX_API_HOST=mmtxapi.hackclub.tv
|
||||
# public ip for webrtc stuff
|
||||
MEDIAMTX_WEBRTC_ADDITIONAL_HOSTS=203.0.113.10
|
||||
|
||||
# mediamtx publish route on hctv
|
||||
MEDIAMTX_AUTH_HTTP_ADDRESS=https://hackclub.tv/api/mediamtx/publish
|
||||
MEDIAMTX_AUTH_HTTP_FINGERPRINT=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.5
|
||||
image: traefik:latest
|
||||
command:
|
||||
- --providers.docker=true
|
||||
- --providers.docker.exposedbydefault=false
|
||||
@@ -10,8 +10,11 @@ services:
|
||||
- --entrypoints.webrtc-ice.address=:8189/udp
|
||||
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
|
||||
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
|
||||
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
|
||||
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
|
||||
- --certificatesresolvers.letsencrypt.acme.dnschallenge=true
|
||||
- --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
|
||||
- --certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,1.0.0.1:53
|
||||
environment:
|
||||
CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN}
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
@@ -23,12 +26,14 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
mediamtx:
|
||||
image: bluenviron/mediamtx:1
|
||||
image: srizan10/hclive-mediamtx
|
||||
volumes:
|
||||
- ./mediamtx.yml:/mediamtx.yml:ro
|
||||
environment:
|
||||
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
|
||||
MTX_WEBRTCADDITIONALHOSTS: ${MEDIAMTX_WEBRTC_ADDITIONAL_HOSTS}
|
||||
MTX_AUTHHTTPADDRESS: ${MEDIAMTX_AUTH_HTTP_ADDRESS}
|
||||
MTX_AUTHHTTPFINGERPRINT: ${MEDIAMTX_AUTH_HTTP_FINGERPRINT:-}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
"sdk:example": "dotenvx run -f .env.sdk -- pnpm --filter=@hctv/sdk example"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.6.2",
|
||||
"turbo": "^2.4.4"
|
||||
"prettier": "^3.8.3",
|
||||
"turbo": "^2.9.7"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.5"
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"lucia": "^3.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
@@ -15,4 +16,4 @@
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
@@ -15,4 +16,4 @@
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/honojs/middleware",
|
||||
"devDependencies": {
|
||||
"@hono/node-server": "^2.0.1",
|
||||
"@types/ws": "^8",
|
||||
"hono": "^4.12.16",
|
||||
"tsup": "^8.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -30,8 +32,8 @@
|
||||
"@hctv/db": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@hono/node-server": "^1.11.1",
|
||||
"hono": "^4.6.0"
|
||||
"@hono/node-server": "^2.0.1",
|
||||
"hono": "^4.12.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"module": "NodeNext",
|
||||
"declaration": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"eslint": "^9.39.2",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.6.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.0.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"ignoreDeprecations": "6.0",
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
@@ -112,4 +113,4 @@
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
11012
pnpm-lock.yaml
generated
11012
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
renovate.json
Normal file
6
renovate.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user