Initial commit
This commit is contained in:
commit
8177684254
47
.devcontainer/devcontainer.json
Normal file
47
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||||
|
{
|
||||||
|
"name": "Existing Docker Compose (Extend)",
|
||||||
|
|
||||||
|
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
|
||||||
|
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
|
||||||
|
"dockerComposeFile": [
|
||||||
|
"../docker-compose.yml",
|
||||||
|
"docker-compose.yml"
|
||||||
|
],
|
||||||
|
|
||||||
|
// The 'service' property is the name of the service for the container that VS Code should
|
||||||
|
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
|
||||||
|
"service": "dev",
|
||||||
|
|
||||||
|
// The optional 'workspaceFolder' property is the path VS Code should open by default when
|
||||||
|
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/python:1": {},
|
||||||
|
"ghcr.io/davzucky/devcontainers-features-wolfi/docker-outside-of-docker:1": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Uncomment the next line if you want start specific services in your Docker Compose config.
|
||||||
|
// "runServices": [],
|
||||||
|
|
||||||
|
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
|
||||||
|
// "shutdownAction": "none",
|
||||||
|
|
||||||
|
// Uncomment the next line to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "cat /etc/os-release",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "devcontainer"
|
||||||
|
}
|
||||||
9
.devcontainer/docker-compose.extend.yml
Normal file
9
.devcontainer/docker-compose.extend.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
cap_add:
|
||||||
|
- SYS_PTRACE
|
||||||
|
command: sleep infinity
|
||||||
|
init: true
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined
|
||||||
|
version: '3.8'
|
||||||
26
.devcontainer/docker-compose.yml
Normal file
26
.devcontainer/docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
# Update this to the name of the service you want to work with in your docker-compose.yml file
|
||||||
|
dev:
|
||||||
|
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
|
||||||
|
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
|
||||||
|
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
|
||||||
|
# array). The sample below assumes your primary file is in the root of your project.
|
||||||
|
#
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: .devcontainer/Dockerfile
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Update this to wherever you want VS Code to mount the folder of your project
|
||||||
|
- ..:/workspaces:cached
|
||||||
|
|
||||||
|
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
|
||||||
|
# cap_add:
|
||||||
|
# - SYS_PTRACE
|
||||||
|
# security_opt:
|
||||||
|
# - seccomp:unconfined
|
||||||
|
|
||||||
|
# Overrides default command so things don't shut down after the process ends.
|
||||||
|
command: sleep infinity
|
||||||
|
|
||||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
|
}
|
||||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for commiting if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
90
Dockerfile
Normal file
90
Dockerfile
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# syntax=docker/dockerfile:1.4
|
||||||
|
ARG NODE_VERSION=20
|
||||||
|
|
||||||
|
# Base development image
|
||||||
|
FROM node:${NODE_VERSION}-slim AS base
|
||||||
|
|
||||||
|
# Install Python and basic build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
build-essential \
|
||||||
|
procps \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create cache directories
|
||||||
|
RUN mkdir -p /root/.npm
|
||||||
|
RUN mkdir -p /root/.pip
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Development stage
|
||||||
|
FROM base AS dev
|
||||||
|
|
||||||
|
# Install development tools
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
vim \
|
||||||
|
ssh \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create a non-root user for development
|
||||||
|
ARG USERNAME=node
|
||||||
|
ARG USER_UID=1000
|
||||||
|
ARG USER_GID=$USER_UID
|
||||||
|
|
||||||
|
# Create the user
|
||||||
|
RUN groupadd --gid $USER_GID $USERNAME \
|
||||||
|
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y sudo \
|
||||||
|
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/$USERNAME
|
||||||
|
|
||||||
|
# Set npm config
|
||||||
|
RUN npm config set cache /root/.npm \
|
||||||
|
&& npm config set prefer-offline true \
|
||||||
|
&& npm config set package-lock true
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY .npmrc ./
|
||||||
|
COPY requirements.txt ./
|
||||||
|
|
||||||
|
# Install Node.js dependencies with cache
|
||||||
|
RUN --mount=type=cache,target=/root/.npm \
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
# Install Python dependencies with cache
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER $USERNAME
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM base AS prod
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY .npmrc ./
|
||||||
|
COPY requirements.txt ./
|
||||||
|
|
||||||
|
# Install production dependencies
|
||||||
|
RUN --mount=type=cache,target=/root/.npm \
|
||||||
|
npm ci --only=production
|
||||||
|
|
||||||
|
# Install Python production dependencies
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production command
|
||||||
|
CMD ["npm", "start"]
|
||||||
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
33
app/components/DonationPanel.tsx
Normal file
33
app/components/DonationPanel.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
interface DonationPanelProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DonationPanel: React.FC<DonationPanelProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<Card className="bg-black/30 border-accent/50 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-accent neon-glow">Support Wildlife</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-foreground/80">Your donation helps protect and preserve the habitats of these amazing creatures.</p>
|
||||||
|
<div className="flex items-center justify-center p-4 bg-accent/10 rounded-md">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08 .402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<Input type="number" placeholder="Amount in USD" className="bg-black/50 border-accent/30 text-foreground" />
|
||||||
|
<Button className="w-full bg-accent text-accent-foreground hover:bg-accent/80">Donate Now</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DonationPanel;
|
||||||
23
app/components/LiveStream.tsx
Normal file
23
app/components/LiveStream.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
interface LiveStreamProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LiveStream: React.FC<LiveStreamProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<Card className="bg-black/30 border-accent/50 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-accent neon-glow">Live Cam {id}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-black aspect-video flex items-center justify-center rounded-md border border-accent/30">
|
||||||
|
<p className="text-accent">Livestream {id} placeholder</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveStream;
|
||||||
24
app/components/OwlInfo.tsx
Normal file
24
app/components/OwlInfo.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
interface OwlInfoProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OwlInfo: React.FC<OwlInfoProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<Card className="bg-black/30 border-accent/50 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-accent neon-glow">About This Livestream</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-foreground/80">
|
||||||
|
This livestream (ID: {id}) showcases the natural habitat and behavior of wildlife in Florida.
|
||||||
|
By observing these animals in their natural environment, we can learn more about their needs and how to protect them.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OwlInfo;
|
||||||
23
app/globals.css
Normal file
23
app/globals.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: 10 10 10;
|
||||||
|
--foreground: 230 230 230;
|
||||||
|
--accent: 0 255 170;
|
||||||
|
--accent-foreground: 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: rgb(var(--foreground));
|
||||||
|
background: rgb(var(--background));
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.neon-glow {
|
||||||
|
text-shadow: 0 0 5px rgb(var(--accent) / 0.5),
|
||||||
|
0 0 10px rgb(var(--accent) / 0.5),
|
||||||
|
0 0 15px rgb(var(--accent) / 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/layout.tsx
Normal file
19
app/layout.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Cape Coral Burrowing Owl Livestream",
|
||||||
|
description: "Live stream of burrowing owls in Cape Coral",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
app/livestream/[id]/page.tsx
Normal file
36
app/livestream/[id]/page.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import LiveStream from '@/app/components/LiveStream';
|
||||||
|
import DonationPanel from '@/app/components/DonationPanel';
|
||||||
|
import OwlInfo from '@/app/components/OwlInfo';
|
||||||
|
|
||||||
|
export default function LivestreamPage({ params }: { params: { id: string } }) {
|
||||||
|
// In a real app, you'd fetch the livestream data based on the ID
|
||||||
|
const streamData = {
|
||||||
|
id: params.id,
|
||||||
|
name: `Livestream ${params.id}`,
|
||||||
|
location: "Florida",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
|
<header className="bg-black/50 backdrop-blur-sm">
|
||||||
|
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 className="text-4xl font-bold neon-glow text-accent">{streamData.name}</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div className="px-4 py-6 sm:px-0">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<LiveStream id={streamData.id} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<DonationPanel id={streamData.id} />
|
||||||
|
<OwlInfo id={streamData.id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
app/page.tsx
Normal file
91
app/page.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
// This would typically come from an API or database
|
||||||
|
const livestreams = [
|
||||||
|
{ id: 1, name: "Cape Coral Burrowing Owl", location: "Cape Coral, FL", status: "Live" },
|
||||||
|
{ id: 2, name: "Sanibel Island Osprey", location: "Sanibel Island, FL", status: "Live" },
|
||||||
|
{ id: 3, name: "Everglades Alligator", location: "Everglades National Park, FL", status: "Offline" },
|
||||||
|
// ... add more livestreams as needed
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
|
<header className="bg-black/50 backdrop-blur-sm sticky top-0 z-10">
|
||||||
|
<div className="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 className="text-3xl font-bold text-accent">Florida Wildlife Livestreams</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div className="px-4 py-6 sm:px-0 space-y-12">
|
||||||
|
<section>
|
||||||
|
<Card className="bg-black/30 border-accent/50 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl text-accent">Welcome to Florida's Wild Side</CardTitle>
|
||||||
|
<CardDescription className="text-foreground/80">
|
||||||
|
Explore the diverse ecosystems of Florida through our live cameras.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-foreground/80 mb-4">
|
||||||
|
From the unique burrowing owls of Cape Coral to the majestic ospreys of Sanibel Island and the iconic alligators of the Everglades, witness the beauty of Florida's wildlife in real-time.
|
||||||
|
</p>
|
||||||
|
<p className="text-foreground/80">
|
||||||
|
Our livestreams offer a window into the natural world, promoting conservation awareness and providing valuable data for researchers and wildlife enthusiasts alike.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-semibold text-accent mb-4">Featured Ecosystems</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{['Cape Coral', 'Sanibel Island', 'Everglades'].map((ecosystem) => (
|
||||||
|
<Card key={ecosystem} className="bg-black/30 border-accent/50 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-accent">{ecosystem}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-foreground/80">
|
||||||
|
{ecosystem === 'Cape Coral' && "Home to the largest population of burrowing owls in Florida, supporting these ground-dwelling birds."}
|
||||||
|
{ecosystem === 'Sanibel Island' && "A barrier island known for its shell beaches and wildlife refuges, crucial for ospreys and coastal birds."}
|
||||||
|
{ecosystem === 'Everglades' && "The largest subtropical wilderness in the US, home to diverse species including the American alligator."}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-semibold text-accent mb-4">Live Cameras</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{livestreams.map((stream) => (
|
||||||
|
<Card key={stream.id} className="bg-black/30 border-accent/50 backdrop-blur-sm hover:bg-black/50 transition-colors">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<CardTitle className="text-accent">{stream.name}</CardTitle>
|
||||||
|
<Badge variant={stream.status === 'Live' ? 'default' : 'secondary'}>
|
||||||
|
{stream.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<CardDescription className="text-foreground/80">{stream.location}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button asChild className="w-full">
|
||||||
|
<Link href={`/livestream/${stream.id}`}>Watch Now</Link>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
components/ui/button.tsx
Normal file
56
components/ui/button.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
55
components/ui/card.tsx
Normal file
55
components/ui/card.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardTitle, CardContent }
|
||||||
25
components/ui/input.tsx
Normal file
25
components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
434
dev_docker_requirements_generator.py
Normal file
434
dev_docker_requirements_generator.py
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
import argparse
|
||||||
|
from typing import Dict, List, Set, Tuple
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class DockerConfigGenerator:
|
||||||
|
def __init__(self, project_path: str):
|
||||||
|
self.project_path = Path(project_path).resolve()
|
||||||
|
self.detected_tech = set()
|
||||||
|
self.node_version = None
|
||||||
|
self.python_version = None
|
||||||
|
self.ports = set()
|
||||||
|
self.env_vars = set()
|
||||||
|
self.dependencies = set()
|
||||||
|
|
||||||
|
def analyze_existing_configs(self) -> None:
|
||||||
|
"""Analyze existing Docker configurations"""
|
||||||
|
# Analyze existing Dockerfile
|
||||||
|
dockerfile_path = self.project_path / 'Dockerfile'
|
||||||
|
if dockerfile_path.exists():
|
||||||
|
with open(dockerfile_path) as f:
|
||||||
|
content = f.read()
|
||||||
|
# Detect base images and versions
|
||||||
|
if 'FROM node:' in content:
|
||||||
|
self.detected_tech.add('nodejs')
|
||||||
|
# Extract node version from FROM statement
|
||||||
|
import re
|
||||||
|
node_match = re.search(r'FROM node:(\d+)', content)
|
||||||
|
if node_match:
|
||||||
|
self.node_version = node_match.group(1)
|
||||||
|
if 'python' in content.lower():
|
||||||
|
self.detected_tech.add('python')
|
||||||
|
|
||||||
|
# Detect common patterns
|
||||||
|
if 'npm ci' in content:
|
||||||
|
self.detected_tech.add('npm')
|
||||||
|
if 'pip install' in content:
|
||||||
|
self.detected_tech.add('pip')
|
||||||
|
|
||||||
|
# Analyze existing docker-compose.yml
|
||||||
|
compose_path = self.project_path / 'docker-compose.yml'
|
||||||
|
if compose_path.exists():
|
||||||
|
with open(compose_path) as f:
|
||||||
|
try:
|
||||||
|
compose_data = yaml.safe_load(f)
|
||||||
|
services = compose_data.get('services', {})
|
||||||
|
|
||||||
|
# Analyze each service
|
||||||
|
for service_name, service_config in services.items():
|
||||||
|
# Extract ports
|
||||||
|
ports = service_config.get('ports', [])
|
||||||
|
for port in ports:
|
||||||
|
if isinstance(port, str):
|
||||||
|
port = port.split(':')[0].replace('${', '').split('-')[-1].replace('}', '')
|
||||||
|
self.ports.add(int(port))
|
||||||
|
|
||||||
|
# Extract environment variables
|
||||||
|
env = service_config.get('environment', [])
|
||||||
|
if isinstance(env, list):
|
||||||
|
for var in env:
|
||||||
|
if isinstance(var, str):
|
||||||
|
self.env_vars.add(var.split('=')[0])
|
||||||
|
elif isinstance(env, dict):
|
||||||
|
self.env_vars.update(env.keys())
|
||||||
|
|
||||||
|
# Extract volumes
|
||||||
|
volumes = service_config.get('volumes', [])
|
||||||
|
for volume in volumes:
|
||||||
|
if isinstance(volume, str):
|
||||||
|
if 'node_modules' in volume:
|
||||||
|
self.detected_tech.add('nodejs')
|
||||||
|
if '.pip' in volume:
|
||||||
|
self.detected_tech.add('python')
|
||||||
|
|
||||||
|
# Extract dependencies
|
||||||
|
depends_on = service_config.get('depends_on', [])
|
||||||
|
self.dependencies.update(depends_on)
|
||||||
|
|
||||||
|
# Extract volume definitions
|
||||||
|
volumes = compose_data.get('volumes', {})
|
||||||
|
for volume_name in volumes:
|
||||||
|
if 'node_modules' in volume_name:
|
||||||
|
self.detected_tech.add('nodejs')
|
||||||
|
if 'pip' in volume_name:
|
||||||
|
self.detected_tech.add('python')
|
||||||
|
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
print(f"Error parsing docker-compose.yml: {e}")
|
||||||
|
|
||||||
|
def detect_technologies(self) -> None:
|
||||||
|
"""Detect technologies used in the project."""
|
||||||
|
# First analyze existing Docker configs
|
||||||
|
self.analyze_existing_configs()
|
||||||
|
|
||||||
|
# Then analyze project files as before
|
||||||
|
if self._find_file('package.json'):
|
||||||
|
self.detected_tech.add('nodejs')
|
||||||
|
with open(self.project_path / 'package.json') as f:
|
||||||
|
package_data = json.load(f)
|
||||||
|
deps = {**package_data.get('dependencies', {}),
|
||||||
|
**package_data.get('devDependencies', {})}
|
||||||
|
if 'react' in deps:
|
||||||
|
self.detected_tech.add('react')
|
||||||
|
if 'express' in deps:
|
||||||
|
self.detected_tech.add('express')
|
||||||
|
if '@tensorflow/tfjs' in deps or 'pytorch' in deps:
|
||||||
|
self.detected_tech.add('ml')
|
||||||
|
if not self.node_version: # Only detect if not already found
|
||||||
|
self._detect_node_version(package_data)
|
||||||
|
|
||||||
|
# Check for Python
|
||||||
|
if self._find_file('requirements.txt') or self._find_file('setup.py'):
|
||||||
|
self.detected_tech.add('python')
|
||||||
|
if self._find_file('requirements.txt'):
|
||||||
|
with open(self.project_path / 'requirements.txt') as f:
|
||||||
|
reqs = f.read()
|
||||||
|
if 'torch' in reqs or 'tensorflow' in reqs:
|
||||||
|
self.detected_tech.add('ml')
|
||||||
|
if 'flask' in reqs:
|
||||||
|
self.ports.add(5000)
|
||||||
|
if 'fastapi' in reqs:
|
||||||
|
self.ports.add(8000)
|
||||||
|
|
||||||
|
# Check for AI/ML specific files
|
||||||
|
if self._find_file('*.onnx', glob=True) or self._find_file('*.pt', glob=True):
|
||||||
|
self.detected_tech.add('ml')
|
||||||
|
|
||||||
|
# Check for environment variables
|
||||||
|
env_file = self._find_file('.env')
|
||||||
|
if env_file:
|
||||||
|
with open(env_file) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.strip() and not line.startswith('#'):
|
||||||
|
key = line.split('=')[0].strip()
|
||||||
|
self.env_vars.add(key)
|
||||||
|
|
||||||
|
def _find_file(self, filename: str, glob: bool = False) -> str:
|
||||||
|
"""Find a file in the project directory."""
|
||||||
|
if glob:
|
||||||
|
for file in self.project_path.glob(filename):
|
||||||
|
return str(file)
|
||||||
|
else:
|
||||||
|
file_path = self.project_path / filename
|
||||||
|
if file_path.exists():
|
||||||
|
return str(file_path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _detect_node_version(self, package_data: Dict) -> None:
|
||||||
|
"""Detect Node.js version from package.json."""
|
||||||
|
engines = package_data.get('engines', {})
|
||||||
|
if 'node' in engines:
|
||||||
|
version = engines['node'].replace('^', '').replace('~', '').split('.')[0]
|
||||||
|
self.node_version = version
|
||||||
|
else:
|
||||||
|
self.node_version = '20' # Default to latest LTS
|
||||||
|
|
||||||
|
def generate_dockerfile(self) -> str:
|
||||||
|
"""Generate Dockerfile content based on detected technologies."""
|
||||||
|
dockerfile = [
|
||||||
|
"# syntax=docker/dockerfile:1.4",
|
||||||
|
f"ARG NODE_VERSION={self.node_version}",
|
||||||
|
"",
|
||||||
|
"# Base development image",
|
||||||
|
"FROM node:${NODE_VERSION}-slim AS base",
|
||||||
|
"",
|
||||||
|
"# Install Python and basic build dependencies",
|
||||||
|
"RUN apt-get update && apt-get install -y \\",
|
||||||
|
" python3 \\",
|
||||||
|
" python3-pip \\",
|
||||||
|
" git \\",
|
||||||
|
" curl \\",
|
||||||
|
" build-essential \\",
|
||||||
|
" procps \\",
|
||||||
|
" && rm -rf /var/lib/apt/lists/*",
|
||||||
|
"",
|
||||||
|
"# Create cache directories",
|
||||||
|
"RUN mkdir -p /root/.npm",
|
||||||
|
"RUN mkdir -p /root/.pip",
|
||||||
|
"",
|
||||||
|
"# Set working directory",
|
||||||
|
"WORKDIR /app",
|
||||||
|
"",
|
||||||
|
"# Development stage",
|
||||||
|
"FROM base AS dev",
|
||||||
|
"",
|
||||||
|
"# Install development tools",
|
||||||
|
"RUN apt-get update && apt-get install -y \\",
|
||||||
|
" vim \\",
|
||||||
|
" ssh \\",
|
||||||
|
" && rm -rf /var/lib/apt/lists/*",
|
||||||
|
"",
|
||||||
|
"# Create a non-root user for development",
|
||||||
|
"ARG USERNAME=node",
|
||||||
|
"ARG USER_UID=1000",
|
||||||
|
"ARG USER_GID=$USER_UID",
|
||||||
|
"",
|
||||||
|
"# Create the user",
|
||||||
|
"RUN groupadd --gid $USER_GID $USERNAME \\",
|
||||||
|
" && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \\",
|
||||||
|
" && apt-get update \\",
|
||||||
|
" && apt-get install -y sudo \\",
|
||||||
|
" && echo $USERNAME ALL=\\(root\\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \\",
|
||||||
|
" && chmod 0440 /etc/sudoers.d/$USERNAME",
|
||||||
|
"",
|
||||||
|
"# Set npm config",
|
||||||
|
"RUN npm config set cache /root/.npm \\",
|
||||||
|
" && npm config set prefer-offline true \\",
|
||||||
|
" && npm config set package-lock true",
|
||||||
|
"",
|
||||||
|
"# Copy package files",
|
||||||
|
"COPY package*.json ./",
|
||||||
|
"COPY .npmrc ./",
|
||||||
|
"COPY requirements.txt ./",
|
||||||
|
"",
|
||||||
|
"# Install Node.js dependencies with cache",
|
||||||
|
"RUN --mount=type=cache,target=/root/.npm \\",
|
||||||
|
" npm ci",
|
||||||
|
"",
|
||||||
|
"# Install Python dependencies with cache",
|
||||||
|
"RUN --mount=type=cache,target=/root/.cache/pip \\",
|
||||||
|
" pip3 install -r requirements.txt",
|
||||||
|
"",
|
||||||
|
"# Switch to non-root user",
|
||||||
|
"USER $USERNAME",
|
||||||
|
"",
|
||||||
|
"# Production stage",
|
||||||
|
"FROM base AS prod",
|
||||||
|
"",
|
||||||
|
"# Copy package files",
|
||||||
|
"COPY package*.json ./",
|
||||||
|
"COPY .npmrc ./",
|
||||||
|
"COPY requirements.txt ./",
|
||||||
|
"",
|
||||||
|
"# Install production dependencies",
|
||||||
|
"RUN --mount=type=cache,target=/root/.npm \\",
|
||||||
|
" npm ci --only=production",
|
||||||
|
"",
|
||||||
|
"# Install Python production dependencies",
|
||||||
|
"RUN --mount=type=cache,target=/root/.cache/pip \\",
|
||||||
|
" pip3 install -r requirements.txt",
|
||||||
|
"",
|
||||||
|
"# Copy application code",
|
||||||
|
"COPY . .",
|
||||||
|
"",
|
||||||
|
"# Build the application",
|
||||||
|
"RUN npm run build",
|
||||||
|
"",
|
||||||
|
"# Production command",
|
||||||
|
'CMD ["npm", "start"]'
|
||||||
|
]
|
||||||
|
|
||||||
|
return '\n'.join(dockerfile)
|
||||||
|
|
||||||
|
def generate_compose(self) -> str:
|
||||||
|
"""Generate docker-compose.yml content."""
|
||||||
|
compose_config = {
|
||||||
|
'version': '3.8',
|
||||||
|
'services': {
|
||||||
|
'dev': {
|
||||||
|
'build': {
|
||||||
|
'context': '.',
|
||||||
|
'target': 'dev',
|
||||||
|
'args': {
|
||||||
|
'NODE_VERSION': self.node_version
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'volumes': [
|
||||||
|
'../:/app:cached',
|
||||||
|
'node_modules:/app/node_modules' if 'nodejs' in self.detected_tech else None,
|
||||||
|
'npm-cache:/root/.npm' if 'nodejs' in self.detected_tech else None,
|
||||||
|
'pip-cache:/root/.cache/pip' if 'python' in self.detected_tech else None,
|
||||||
|
'~/.gitconfig:/root/.gitconfig',
|
||||||
|
'~/.ssh:/root/.ssh'
|
||||||
|
],
|
||||||
|
'environment': list(self.env_vars) if self.env_vars else [
|
||||||
|
'NODE_ENV=development',
|
||||||
|
'PORT=${DEV_PORT_1:-3000}',
|
||||||
|
'BACKEND_PORT=${DEV_PORT_2:-5000}',
|
||||||
|
],
|
||||||
|
'ports': [f"{port}:{port}" for port in self.ports] or [
|
||||||
|
'${DEV_PORT_1:-3000}:3000',
|
||||||
|
'${DEV_PORT_2:-5000}:5000',
|
||||||
|
'9229:9229'
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'volumes': {
|
||||||
|
'node_modules': {},
|
||||||
|
'npm-cache': {},
|
||||||
|
'pip-cache': {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add special services based on dependencies
|
||||||
|
compose_config = self.add_special_services(compose_config)
|
||||||
|
|
||||||
|
# Remove None values
|
||||||
|
compose_config['services']['dev']['volumes'] = [v for v in compose_config['services']['dev']['volumes'] if v]
|
||||||
|
|
||||||
|
return yaml.dump(compose_config, default_flow_style=False)
|
||||||
|
|
||||||
|
def generate_devcontainer(self) -> str:
|
||||||
|
"""Generate devcontainer.json content."""
|
||||||
|
config = {
|
||||||
|
"name": "Development Environment",
|
||||||
|
"dockerComposeFile": [
|
||||||
|
"../docker-compose.yml",
|
||||||
|
"docker-compose.extend.yml"
|
||||||
|
],
|
||||||
|
"service": "app",
|
||||||
|
"workspaceFolder": "/app",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"editorconfig.editorconfig"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": list(self.ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add language-specific extensions
|
||||||
|
if 'nodejs' in self.detected_tech:
|
||||||
|
config["customizations"]["vscode"]["extensions"].extend([
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"christian-kohler.npm-intellisense"
|
||||||
|
])
|
||||||
|
|
||||||
|
if 'python' in self.detected_tech:
|
||||||
|
config["customizations"]["vscode"]["extensions"].extend([
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance"
|
||||||
|
])
|
||||||
|
|
||||||
|
return json.dumps(config, indent=2)
|
||||||
|
|
||||||
|
def generate_devcontainer_compose(self) -> str:
|
||||||
|
"""Generate docker-compose.extend.yml content for devcontainer."""
|
||||||
|
config = {
|
||||||
|
'version': '3.8',
|
||||||
|
'services': {
|
||||||
|
'app': {
|
||||||
|
'init': True,
|
||||||
|
'security_opt': ['seccomp:unconfined'],
|
||||||
|
'cap_add': ['SYS_PTRACE'],
|
||||||
|
'command': 'sleep infinity'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return yaml.dump(config, default_flow_style=False)
|
||||||
|
|
||||||
|
def generate_configs(self) -> None:
|
||||||
|
"""Generate all configuration files."""
|
||||||
|
# Create backup directory
|
||||||
|
backup_dir = self.project_path / 'docker_config_backups'
|
||||||
|
backup_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Backup existing files
|
||||||
|
for filename in ['Dockerfile', 'docker-compose.yml']:
|
||||||
|
filepath = self.project_path / filename
|
||||||
|
if filepath.exists():
|
||||||
|
backup_path = backup_dir / f"{filename}.backup"
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(filepath, backup_path)
|
||||||
|
print(f"Backed up {filename} to {backup_path}")
|
||||||
|
|
||||||
|
# Create .devcontainer directory if it doesn't exist
|
||||||
|
devcontainer_dir = self.project_path / '.devcontainer'
|
||||||
|
devcontainer_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Generate and write new files
|
||||||
|
with open(self.project_path / 'Dockerfile', 'w') as f:
|
||||||
|
f.write(self.generate_dockerfile())
|
||||||
|
|
||||||
|
with open(self.project_path / 'docker-compose.yml', 'w') as f:
|
||||||
|
f.write(self.generate_compose())
|
||||||
|
|
||||||
|
with open(devcontainer_dir / 'devcontainer.json', 'w') as f:
|
||||||
|
f.write(self.generate_devcontainer())
|
||||||
|
|
||||||
|
with open(devcontainer_dir / 'docker-compose.extend.yml', 'w') as f:
|
||||||
|
f.write(self.generate_devcontainer_compose())
|
||||||
|
|
||||||
|
def add_special_services(self, compose_config: dict) -> dict:
|
||||||
|
"""Add special services based on detected dependencies."""
|
||||||
|
for dependency in self.dependencies:
|
||||||
|
if dependency == 'ollama':
|
||||||
|
compose_config['services']['ollama'] = {
|
||||||
|
'image': 'ollama/ollama',
|
||||||
|
'volumes': ['ollama-models:/root/.ollama'],
|
||||||
|
'ports': ['${OLLAMA_PORT_1:-11434}:11434']
|
||||||
|
}
|
||||||
|
compose_config['volumes']['ollama-models'] = {}
|
||||||
|
|
||||||
|
elif dependency == 'sdwebui':
|
||||||
|
compose_config['services']['sdwebui'] = {
|
||||||
|
'image': 'stable-diffusion-webui/stable-diffusion-webui',
|
||||||
|
'volumes': ['sd-models:/models'],
|
||||||
|
'ports': ['${SDWEBUI_PORT_1:-7860}:7860']
|
||||||
|
}
|
||||||
|
compose_config['volumes']['sd-models'] = {}
|
||||||
|
|
||||||
|
return compose_config
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Generate Docker configurations for a project')
|
||||||
|
parser.add_argument('project_path', help='Path to the project directory')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
generator = DockerConfigGenerator(args.project_path)
|
||||||
|
print("🔍 Analyzing project...")
|
||||||
|
generator.detect_technologies()
|
||||||
|
|
||||||
|
print("\n📦 Detected technologies:")
|
||||||
|
for tech in generator.detected_tech:
|
||||||
|
print(f" - {tech}")
|
||||||
|
|
||||||
|
print("\n🔧 Generating configuration files...")
|
||||||
|
generator.generate_configs()
|
||||||
|
|
||||||
|
print("\n✅ Generated files:")
|
||||||
|
print(" - Dockerfile")
|
||||||
|
print(" - docker-compose.yml")
|
||||||
|
print(" - .devcontainer/devcontainer.json")
|
||||||
|
print(" - .devcontainer/docker-compose.extend.yml")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
dev:
|
||||||
|
build:
|
||||||
|
args:
|
||||||
|
NODE_VERSION: '20'
|
||||||
|
context: .
|
||||||
|
target: dev
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- PORT=${DEV_PORT_1:-3000}
|
||||||
|
- BACKEND_PORT=${DEV_PORT_2:-5000}
|
||||||
|
ports:
|
||||||
|
- ${DEV_PORT_1:-3000}:3000
|
||||||
|
- ${DEV_PORT_2:-5000}:5000
|
||||||
|
- 9229:9229
|
||||||
|
volumes:
|
||||||
|
- ../:/app:cached
|
||||||
|
- node_modules:/app/node_modules
|
||||||
|
- npm-cache:/root/.npm
|
||||||
|
- ~/.gitconfig:/root/.gitconfig
|
||||||
|
- ~/.ssh:/root/.ssh
|
||||||
|
version: '3.8'
|
||||||
|
volumes:
|
||||||
|
node_modules: {}
|
||||||
|
npm-cache: {}
|
||||||
|
pip-cache: {}
|
||||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
6
next.config.js
Normal file
6
next.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
6958
package-lock.json
generated
Normal file
6958
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "owl-stream",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"next": "13.5.6",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"shadcn-ui": "^0.9.2",
|
||||||
|
"tailwind-merge": "^1.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"autoprefixer": "^10",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "13.5.6",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
public/file.svg
Normal file
1
public/file.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
1
public/globe.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/next.svg
Normal file
1
public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
1
public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
1
public/window.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
src/app/fonts/GeistMonoVF.woff
Normal file
BIN
src/app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
src/app/fonts/GeistVF.woff
Normal file
BIN
src/app/fonts/GeistVF.woff
Normal file
Binary file not shown.
21
src/app/globals.css
Normal file
21
src/app/globals.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--background);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
35
src/app/layout.tsx
Normal file
35
src/app/layout.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = localFont({
|
||||||
|
src: "./fonts/GeistVF.woff",
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
weight: "100 900",
|
||||||
|
});
|
||||||
|
const geistMono = localFont({
|
||||||
|
src: "./fonts/GeistMonoVF.woff",
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
weight: "100 900",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
src/app/page.tsx
Normal file
101
src/app/page.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||||
|
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/next.svg"
|
||||||
|
alt="Next.js logo"
|
||||||
|
width={180}
|
||||||
|
height={38}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||||
|
<li className="mb-2">
|
||||||
|
Get started by editing{" "}
|
||||||
|
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||||
|
src/app/page.tsx
|
||||||
|
</code>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
<li>Save and see your changes instantly.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||||
|
<a
|
||||||
|
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||||
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/vercel.svg"
|
||||||
|
alt="Vercel logomark"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
Deploy now
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||||
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Read our docs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
aria-hidden
|
||||||
|
src="/file.svg"
|
||||||
|
alt="File icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Learn
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
aria-hidden
|
||||||
|
src="/window.svg"
|
||||||
|
alt="Window icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Examples
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
|
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
aria-hidden
|
||||||
|
src="/globe.svg"
|
||||||
|
alt="Globe icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Go to nextjs.org →
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
tailwind.config.js
Normal file
19
tailwind.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: 'rgb(var(--background) / <alpha-value>)',
|
||||||
|
foreground: 'rgb(var(--foreground) / <alpha-value>)',
|
||||||
|
accent: 'rgb(var(--accent) / <alpha-value>)',
|
||||||
|
'accent-foreground': 'rgb(var(--accent-foreground) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
18
tailwind.config.ts
Normal file
18
tailwind.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: "var(--background)",
|
||||||
|
foreground: "var(--foreground)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user