Compare commits
9 Commits
2bc6d1bdb0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
442d53c9d1
|
|||
|
94e921c900
|
|||
|
c1bcc2ce4a
|
|||
|
496ed2cb00
|
|||
|
d9622ea451
|
|||
|
cfd2e0d2c2
|
|||
|
8bce0bd957
|
|||
|
ddf47ff22d
|
|||
|
09f6f49390
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.env*.local
|
||||
|
||||
6
.vercelignore
Normal file
6
.vercelignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
.git
|
||||
.vercel
|
||||
api
|
||||
cdk
|
||||
@@ -1 +1,5 @@
|
||||
## Godot Host V2
|
||||
## Project Hoster
|
||||
|
||||
### Attribution
|
||||
This project is essentially a clone of https://godothost.vercel.app, but with less features.
|
||||
This project exists as im unaware if the original is maintained, and when I brought up the idea to cs, he preferred a rewrite.
|
||||
|
||||
44
api/Cargo.lock
generated
44
api/Cargo.lock
generated
@@ -19,6 +19,21 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-cors"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
|
||||
dependencies = [
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"derive_more",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.11.2"
|
||||
@@ -243,6 +258,7 @@ dependencies = [
|
||||
name = "api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-cors",
|
||||
"actix-web",
|
||||
"aws-config",
|
||||
"aws-sdk-dynamodb",
|
||||
@@ -1380,6 +1396,17 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
@@ -1400,6 +1427,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
@@ -2916,6 +2944,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.13",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
@@ -2938,12 +2967,14 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
@@ -4379,6 +4410,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.83"
|
||||
|
||||
@@ -4,13 +4,14 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-cors = "0.7.1"
|
||||
actix-web = "4.12.1"
|
||||
aws-config = "1.8.12"
|
||||
aws-sdk-dynamodb = "1.102.0"
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
|
||||
mime_guess = "2.0.5"
|
||||
reqwest = { version = "0.13.1", features = ["json", "query"] }
|
||||
reqwest = { version = "0.13.1", features = ["json", "query", "stream"] }
|
||||
sentry = { version = "0.46.1", features = ["actix", "tracing"] }
|
||||
serde = "1.0.228"
|
||||
serde_dynamo = { version = "4.3.0", features = ["aws-sdk-dynamodb+1"] }
|
||||
|
||||
13
api/Dockerfile
Normal file
13
api/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM rust:1.92-alpine3.20 AS builder
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN apk update && apk add musl-dev libressl-dev
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
FROM alpine:3.20
|
||||
|
||||
COPY --from=builder /target/release/api /usr/local/bin/api
|
||||
|
||||
CMD ["/usr/local/bin/api"]
|
||||
@@ -42,7 +42,6 @@ pub async fn get_repos(
|
||||
.into_iter()
|
||||
.map(|r| r.id)
|
||||
.collect::<HashSet<String>>();
|
||||
println!("{added_ids:?}");
|
||||
let data = response
|
||||
.json::<Vec<Repository>>()
|
||||
.await?
|
||||
|
||||
@@ -33,9 +33,11 @@ pub async fn proxy_file(
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
response.error_for_status_ref()?;
|
||||
response
|
||||
.error_for_status_ref()
|
||||
.map_err(|_| crate::error::Error::NotFound)?;
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
let stream = response.bytes_stream();
|
||||
let mime = mime_guess::from_path(&path.file)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
@@ -43,5 +45,5 @@ pub async fn proxy_file(
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(mime)
|
||||
.insert_header(("Cache-Control", "public, max-age=3600"))
|
||||
.body(bytes))
|
||||
.streaming(stream))
|
||||
}
|
||||
|
||||
@@ -35,11 +35,5 @@ pub async fn search_repos(
|
||||
.json::<SearchResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(
|
||||
response
|
||||
.items
|
||||
.into_iter()
|
||||
.filter(|r| r.private == false)
|
||||
.collect::<Vec<Repository>>(),
|
||||
))
|
||||
Ok(HttpResponse::Ok().json(response.items))
|
||||
}
|
||||
|
||||
@@ -54,7 +54,9 @@ impl ResponseError for Error {
|
||||
Error::ValidationFailed(msg) => {
|
||||
HttpResponse::BadRequest().json(ErrorResponse { error: msg.clone() })
|
||||
}
|
||||
Error::NotFound => HttpResponse::NotFound().finish(),
|
||||
Error::NotFound => HttpResponse::NotFound().json(ErrorResponse {
|
||||
error: "not found".to_string(),
|
||||
}),
|
||||
_ => HttpResponse::InternalServerError().finish(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ async fn run() -> std::io::Result<()> {
|
||||
.finish(),
|
||||
)
|
||||
.wrap(tracing_actix_web::TracingLogger::default())
|
||||
.wrap(actix_cors::Cors::permissive())
|
||||
.route(
|
||||
"/",
|
||||
web::get()
|
||||
@@ -97,7 +98,7 @@ async fn run() -> std::io::Result<()> {
|
||||
),
|
||||
)
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.bind((env::var("ADDRESS").unwrap_or("0.0.0.0".to_string()), 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl UserRepository {
|
||||
.item("gsi1pk", AttributeValue::S("REPOS".into()))
|
||||
.item("gsi1sk", AttributeValue::S(now.clone()))
|
||||
.item("imported_at", AttributeValue::S(now))
|
||||
.item("approved", AttributeValue::Bool(false))
|
||||
.item("approved", AttributeValue::Bool(true))
|
||||
.item("owner_id", AttributeValue::S(user_id.into()))
|
||||
.item("description", AttributeValue::S(repo.description.clone()))
|
||||
.send()
|
||||
|
||||
@@ -4,5 +4,9 @@ import { GhostV2Stack } from '../lib/cdk-stack';
|
||||
const app = new cdk.App();
|
||||
|
||||
new GhostV2Stack(app, "GhostV2Stack-dev", {
|
||||
env: { region: "ca-central-1", account: process.env.AWS_ACCOUNT_ID! },
|
||||
env: { region: "ca-central-1", account: process.env.AWS_ACCOUNT_ID! }, environment: "dev"
|
||||
});
|
||||
|
||||
new GhostV2Stack(app, "GhostV2Stack-prod", {
|
||||
env: { region: "ca-central-1", account: process.env.AWS_ACCOUNT_ID!, }, environment: "prod"
|
||||
})
|
||||
|
||||
11
cdk/cdk.context.json
Normal file
11
cdk/cdk.context.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"availability-zones:account=585061171043:region=ca-central-1": [
|
||||
"ca-central-1a",
|
||||
"ca-central-1b",
|
||||
"ca-central-1d"
|
||||
],
|
||||
"hosted-zone:account=585061171043:domainName=lucalise.ca:region=ca-central-1": {
|
||||
"Id": "/hostedzone/Z0948300LINP3SX1WI4O",
|
||||
"Name": "lucalise.ca."
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,136 @@
|
||||
import * as cdk from 'aws-cdk-lib/core';
|
||||
import { Construct } from 'constructs';
|
||||
import * as cdk from "aws-cdk-lib/core";
|
||||
import { Construct } from "constructs";
|
||||
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
|
||||
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
||||
import * as ecs from "aws-cdk-lib/aws-ecs";
|
||||
import * as ssm from "aws-cdk-lib/aws-ssm"
|
||||
import * as acm from "aws-cdk-lib/aws-certificatemanager"
|
||||
import * as route53 from "aws-cdk-lib/aws-route53"
|
||||
import * as route53Targets from "aws-cdk-lib/aws-route53-targets"
|
||||
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"
|
||||
import * as path from "path";
|
||||
|
||||
interface StackProps extends cdk.StackProps {
|
||||
environment: string
|
||||
}
|
||||
|
||||
export class GhostV2Stack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
constructor(scope: Construct, id: string, props: StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
const table = new dynamodb.TableV2(this, "ghostv2-table", {
|
||||
partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
|
||||
sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
|
||||
billing: dynamodb.Billing.onDemand(),
|
||||
})
|
||||
});
|
||||
table.addGlobalSecondaryIndex({
|
||||
indexName: "gsi1",
|
||||
partitionKey: { name: "gsi1pk", type: dynamodb.AttributeType.STRING },
|
||||
sortKey: { name: "gsi1sk", type: dynamodb.AttributeType.STRING }
|
||||
sortKey: { name: "gsi1sk", type: dynamodb.AttributeType.STRING },
|
||||
});
|
||||
if (props.environment !== "prod") {
|
||||
return;
|
||||
}
|
||||
|
||||
const vpc = new ec2.Vpc(this, "ghostv2 vpc", {
|
||||
maxAzs: 2,
|
||||
natGateways: 0,
|
||||
subnetConfiguration: [
|
||||
{
|
||||
name: "Public",
|
||||
subnetType: ec2.SubnetType.PUBLIC,
|
||||
cidrMask: 24,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const ecsSecurityGroup = new ec2.SecurityGroup(this, "ghostv2 security group", {
|
||||
vpc,
|
||||
description: "Security group for ECS Fargate tasks",
|
||||
allowAllOutbound: true,
|
||||
});
|
||||
ecsSecurityGroup.addIngressRule(
|
||||
ec2.Peer.anyIpv4(),
|
||||
ec2.Port.tcp(80),
|
||||
"Allow HTTP"
|
||||
);
|
||||
ecsSecurityGroup.addIngressRule(
|
||||
ec2.Peer.anyIpv4(),
|
||||
ec2.Port.tcp(443),
|
||||
"Allow HTTPS"
|
||||
);
|
||||
ecsSecurityGroup.addIngressRule(
|
||||
ec2.Peer.anyIpv4(),
|
||||
ec2.Port.tcp(8080),
|
||||
"Allow API"
|
||||
);
|
||||
|
||||
const cluster = new ecs.Cluster(this, "ghostv2 cluster", {
|
||||
vpc,
|
||||
});
|
||||
const taskDefinition = new ecs.FargateTaskDefinition(
|
||||
this,
|
||||
"ghostv2 api",
|
||||
{
|
||||
memoryLimitMiB: 512,
|
||||
cpu: 256,
|
||||
runtimePlatform: {
|
||||
cpuArchitecture: ecs.CpuArchitecture.X86_64,
|
||||
operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
|
||||
},
|
||||
}
|
||||
);
|
||||
const secretImports = ["DATABASE_URL", "GH_CLIENT_ID", "GH_CLIENT_SECRET", "REPOS_TABLE_NAME", "SENTRY_DSN", "FRONTEND_BASE_URL"];
|
||||
const secrets = secretImports.reduce<Record<string, ecs.Secret>>((acc, name) => {
|
||||
acc[name] = ecs.Secret.fromSsmParameter(ssm.StringParameter.fromSecureStringParameterAttributes(this, `param${name}`, {
|
||||
parameterName: `/ghostv2/${props.environment}/${name}`
|
||||
}))
|
||||
return acc
|
||||
}, {})
|
||||
taskDefinition.addContainer("api", {
|
||||
image: ecs.ContainerImage.fromAsset(path.join(__dirname, "../../api")),
|
||||
environment: {
|
||||
RUST_LOG: "info",
|
||||
},
|
||||
portMappings: [
|
||||
{
|
||||
containerPort: 8080,
|
||||
protocol: ecs.Protocol.TCP,
|
||||
},
|
||||
],
|
||||
secrets
|
||||
});
|
||||
table.grantReadWriteData(taskDefinition.taskRole);
|
||||
const service = new ecs.FargateService(this, "ghostv2 service", {
|
||||
cluster,
|
||||
taskDefinition,
|
||||
desiredCount: 1,
|
||||
assignPublicIp: true,
|
||||
vpcSubnets: {
|
||||
subnetType: ec2.SubnetType.PUBLIC,
|
||||
},
|
||||
securityGroups: [ecsSecurityGroup],
|
||||
capacityProviderStrategies: [
|
||||
{
|
||||
capacityProvider: "FARGATE_SPOT",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
const hostedZone = route53.HostedZone.fromLookup(this, "hosted zone", { domainName: "lucalise.ca" })
|
||||
const certificate = new acm.Certificate(this, "ghostv2 api cert", {
|
||||
domainName: "api.ghostv2.lucalise.ca",
|
||||
validation: acm.CertificateValidation.fromDns(hostedZone)
|
||||
})
|
||||
const alb = new elbv2.ApplicationLoadBalancer(this, "ghostv2 alb", {
|
||||
vpc,
|
||||
internetFacing: true,
|
||||
securityGroup: ecsSecurityGroup,
|
||||
});
|
||||
alb.setAttribute("idle_timeout.timeout_seconds", "120")
|
||||
alb.addRedirect({ sourcePort: 80, sourceProtocol: elbv2.ApplicationProtocol.HTTP, targetPort: 443, targetProtocol: elbv2.ApplicationProtocol.HTTPS })
|
||||
const listener = alb.addListener("https listener", { port: 443, open: true, certificates: [certificate] })
|
||||
listener.addTargets("ECSTargets", { port: 8080, protocol: elbv2.ApplicationProtocol.HTTP, targets: [service], healthCheck: { path: "/", port: "8080" } })
|
||||
new route53.ARecord(this, "alb record", { zone: hostedZone, recordName: "api.ghostv2.lucalise.ca", target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(alb)) })
|
||||
}
|
||||
}
|
||||
|
||||
27
scripts/import-env.sh
Executable file
27
scripts/import-env.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PREFIX="/ghostv2"
|
||||
REGION="ca-central-1"
|
||||
WRITE="$1"
|
||||
|
||||
while getopts "w" opt; do
|
||||
case $opt in
|
||||
w) WRITE=true ;;
|
||||
*) echo "Usage $0 [-w]" && exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
while IFS='=' read -r key value; do
|
||||
[[ -z "$key" || "$key" =~ ^# ]] && continue
|
||||
|
||||
echo "Creating parameter: $PREFIX/$key"
|
||||
|
||||
if [[ "$WRITE" == "true" ]]; then
|
||||
aws ssm put-parameter \
|
||||
--name "$PREFIX/$key" \
|
||||
--value "$value" \
|
||||
--type SecureString \
|
||||
--overwrite \
|
||||
--region "$REGION" > /dev/null
|
||||
fi
|
||||
done < api/.env
|
||||
9
src/lib/Footer.svelte
Normal file
9
src/lib/Footer.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="mt-24 flex justify-center border-t border-t-gray-800 p-2">
|
||||
<a
|
||||
class="rounded-sm p-2 transition-colors hover:bg-surface-muted"
|
||||
href="https://git.lucalise.ca/lucalise/ghostv2"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="text-sm text-text-muted">Source & Attribution</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Avatar, Button, DropdownMenu, Separator } from 'bits-ui';
|
||||
import { authClient } from './auth-client';
|
||||
import Image from './Image.svelte';
|
||||
import { CircleUser, FolderGit2, User } from '@lucide/svelte';
|
||||
import { CircleUser, FolderGit2, Loader2, User } from '@lucide/svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
type MockUser = {
|
||||
image: string;
|
||||
@@ -10,22 +10,31 @@
|
||||
|
||||
const session = authClient.useSession();
|
||||
const user = $derived($session.data?.user);
|
||||
let loading = $state(false);
|
||||
|
||||
const signIn = async () => {
|
||||
await authClient.signIn.social({
|
||||
loading = true;
|
||||
const { error } = await authClient.signIn.social({
|
||||
provider: 'github'
|
||||
});
|
||||
if (error) {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="h-14 w-full bg-[#292e42] sm:p-2">
|
||||
<div class="mx-auto flex h-full max-w-312 items-center justify-between">
|
||||
<div class="flex h-14 w-full items-center bg-[#292e42] sm:p-2">
|
||||
<div class="mx-auto flex h-full max-w-312 flex-1 items-center justify-between">
|
||||
<Button.Root onclick={() => goto('/')}>
|
||||
<h1 class="header-title font-light">Project Host</h1>
|
||||
</Button.Root>
|
||||
{#if !user && !$session.isPending}
|
||||
<Button.Root class="rounded-md p-2 transition-colors hover:bg-gray-800" onclick={signIn}>
|
||||
Sign In
|
||||
{#if loading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
Sign In
|
||||
{/if}
|
||||
</Button.Root>
|
||||
{:else if user}
|
||||
<DropdownMenu.Root>
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
type ImageStatus = 'loading' | 'loaded' | 'error';
|
||||
import type { HTMLImgAttributes } from 'svelte/elements';
|
||||
|
||||
let { src, ...props }: HTMLImgAttributes = $props();
|
||||
let { src, fallback, ...props }: HTMLImgAttributes & { fallback?: string } = $props();
|
||||
let status: ImageStatus = $state('loading');
|
||||
let useFallback = $state(false);
|
||||
|
||||
const currentSrc = $derived(useFallback && fallback ? fallback : src);
|
||||
</script>
|
||||
|
||||
{#if status === 'loading'}
|
||||
@@ -11,11 +14,18 @@
|
||||
{/if}
|
||||
|
||||
<img
|
||||
{src}
|
||||
src={currentSrc}
|
||||
hidden={status === 'loading'}
|
||||
onload={() => {
|
||||
status = 'loaded';
|
||||
}}
|
||||
onerror={() => (status = 'error')}
|
||||
onerror={() => {
|
||||
if (!useFallback && fallback) {
|
||||
useFallback = true;
|
||||
status = 'loading';
|
||||
} else {
|
||||
status = 'error';
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
>
|
||||
<div class="relative aspect-video overflow-hidden">
|
||||
<Image
|
||||
src={'https://picsum.photos/seed/yama/400/225'}
|
||||
src={`/api/v0/repos/${project.id}/files/thumbnail.webp`}
|
||||
fallback={`https://picsum.photos/seed/${project.id}/400/225`}
|
||||
alt={project.full_name}
|
||||
class="min-h-56.5 w-full min-w-100 object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
|
||||
@@ -115,7 +115,6 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<GitBranch class="h-6 w-6 text-icon" />
|
||||
<h2 class="text-xl font-semibold text-text-primary">Import Git Repository</h2>
|
||||
<span class="text-xs text-text-muted">repo must be public</span>
|
||||
</div>
|
||||
<Button.Root
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary"
|
||||
|
||||
@@ -2,6 +2,6 @@ import { jwtClient, adminClient } from 'better-auth/client/plugins';
|
||||
import { createAuthClient } from 'better-auth/svelte';
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: 'http://localhost:5173',
|
||||
plugins: [jwtClient(), adminClient()]
|
||||
baseURL: process.env.BETTER_AUTH_URL!,
|
||||
plugins: [jwtClient(), adminClient()]
|
||||
});
|
||||
|
||||
@@ -6,16 +6,22 @@
|
||||
import NProgress from 'nprogress';
|
||||
import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools';
|
||||
import { Toaster } from 'svelte-sonner';
|
||||
import Footer from '$lib/Footer.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
const queryClient = new QueryClient();
|
||||
NProgress.configure({ showSpinner: false });
|
||||
</script>
|
||||
|
||||
<Header />
|
||||
<Toaster position="top-center" theme="dark" />
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{@render children()}
|
||||
<SvelteQueryDevtools />
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<main class="flex-1">
|
||||
{@render children()}
|
||||
</main>
|
||||
<SvelteQueryDevtools />
|
||||
<Footer />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import Projects from '$lib/Projects.svelte';
|
||||
</script>
|
||||
|
||||
<main class="mt-8 flex flex-col items-center justify-center">
|
||||
<section class="mt-8 flex flex-1 flex-col items-center justify-center">
|
||||
<h1 class="flex justify-center text-2xl font-semibold">Projects</h1>
|
||||
|
||||
<section class="m-4 mt-4 max-w-4xl border-t-4 border-gray-700/80 p-4 px-12 pt-6">
|
||||
<Projects />
|
||||
</section>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
import Repos from '$lib/Repos.svelte';
|
||||
</script>
|
||||
|
||||
<main class="mx-auto mt-8 max-w-3xl px-4">
|
||||
<section class="mx-auto mt-8 max-w-3xl px-4">
|
||||
<Repos />
|
||||
</main>
|
||||
</section>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color-scheme: dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
/* background-color: #242424; */
|
||||
|
||||
@@ -159,17 +159,17 @@ button:focus-visible {
|
||||
animation: grow-out 150ms ease-out;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:default:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button:default {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
/* @media (prefers-color-scheme: light) { */
|
||||
/* :root { */
|
||||
/* color: #213547; */
|
||||
/* background-color: #ffffff; */
|
||||
/* } */
|
||||
/**/
|
||||
/* a:default:hover { */
|
||||
/* color: #747bff; */
|
||||
/* } */
|
||||
/**/
|
||||
/* button:default { */
|
||||
/* background-color: #f9f9f9; */
|
||||
/* } */
|
||||
/* } */
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<svelte:window onkeydown={minimize} />
|
||||
<svelte:document onfullscreenchange={handleFullscreenChange} />
|
||||
|
||||
<main class="flex min-h-screen flex-col items-center px-4 py-8">
|
||||
<section class="flex min-h-screen flex-col items-center px-4 py-8">
|
||||
<div class="relative mb-6 flex w-full max-w-6xl items-center justify-center">
|
||||
<Button.Root
|
||||
class="absolute left-0 flex items-center gap-2 rounded-md px-3 py-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<div
|
||||
bind:this={containerRef}
|
||||
class="relative w-full max-w-6xl overflow-hidden rounded-md bg-black shadow-2xl ring-1 ring-gray-700/80"
|
||||
class="relative mt-2 w-full max-w-6xl overflow-hidden rounded-md bg-black shadow-2xl ring-1 ring-gray-700/80"
|
||||
class:max-w-none={isFullscreen}
|
||||
class:h-screen={isFullscreen}
|
||||
class:rounded-none={isFullscreen}
|
||||
@@ -84,4 +84,4 @@
|
||||
</Button.Root>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<script>
|
||||
import { GitBranch, Search } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
<main class="mx-auto mt-8 max-w-3xl rounded-xl bg-surface p-4">
|
||||
<div class="flex items-center gap-3 p-6">
|
||||
<GitBranch />
|
||||
<h2 class="text-xl font-semibold">Repositories</h2>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<Search class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
||||
<input
|
||||
class="w-full rounded-lg border border-border-hover bg-surface-hover py-2 pl-10 outline-none focus:border-border-focus focus:ring focus:ring-border-focus"
|
||||
placeholder="Search for repositories..."
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
8
vercel.json
Normal file
8
vercel.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/api/v0/:path*",
|
||||
"destination": "https://api.ghostv2.lucalise.ca/api/v0/:path*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user