Compare commits
5 Commits
main
...
feature/ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f34251198 | ||
| da52852fdf | |||
| 7b23b296d3 | |||
| a4a5d03f9f | |||
| 11e973ce61 |
75
API_ENDPOINTS.md
Normal file
@ -0,0 +1,75 @@
|
||||
# API-Endpunkte
|
||||
|
||||
Diese Endpunkte decken die Kernfunktionen von OnlyPrompt ab.
|
||||
|
||||
## Auth
|
||||
|
||||
### `POST /api/v1/auth/register`
|
||||
- Erstellt einen neuen Benutzer.
|
||||
|
||||
### `POST /api/v1/auth/login`
|
||||
- Loggt einen Benutzer ein.
|
||||
|
||||
### `GET /api/v1/auth/me`
|
||||
- Gibt den aktuell eingeloggten Benutzer zurueck.
|
||||
|
||||
## User / Profile
|
||||
|
||||
### `GET /api/v1/users/{id}`
|
||||
- Ruft das oeffentliche Benutzerprofil anhand der ID ab.
|
||||
|
||||
### `GET /api/v1/profile/me`
|
||||
- Ruft das eigene Profil des eingeloggten Benutzers ab.
|
||||
|
||||
### `PUT /api/v1/profile/me`
|
||||
- Bearbeitet das eigene Profil des eingeloggten Benutzers.
|
||||
|
||||
## Prompts
|
||||
|
||||
### `GET /api/v1/prompts`
|
||||
- Gibt alle veroeffentlichten Prompts zurueck.
|
||||
|
||||
### `GET /api/v1/prompts/{id}`
|
||||
- Ruft einen einzelnen Prompt anhand der ID ab.
|
||||
|
||||
### `POST /api/v1/prompts`
|
||||
- Erstellt einen neuen Prompt.
|
||||
|
||||
### `PUT /api/v1/prompts/{id}`
|
||||
- Bearbeitet einen bestehenden Prompt.
|
||||
|
||||
### `DELETE /api/v1/prompts/{id}`
|
||||
- Loescht einen Prompt.
|
||||
|
||||
## Filter fuer Marketplace
|
||||
|
||||
### `GET /api/v1/prompts?category=coding`
|
||||
- Filtert Prompts nach Kategorie.
|
||||
|
||||
### `GET /api/v1/prompts?search=python`
|
||||
- Sucht Prompts ueber einen Suchbegriff.
|
||||
|
||||
### `GET /api/v1/prompts?creatorId=5`
|
||||
- Filtert Prompts nach einem bestimmten Creator.
|
||||
|
||||
## Kategorien
|
||||
|
||||
### `GET /api/v1/categories`
|
||||
- Gibt alle verfuegbaren Kategorien zurueck.
|
||||
|
||||
## Kaeufe
|
||||
|
||||
### `POST /api/v1/purchases`
|
||||
- Erstellt einen Kauf fuer einen Prompt.
|
||||
|
||||
### `GET /api/v1/purchases/me`
|
||||
- Gibt alle eigenen Kaeufe des eingeloggten Benutzers zurueck.
|
||||
|
||||
## Reviews
|
||||
|
||||
### `GET /api/v1/prompts/{id}/reviews`
|
||||
- Ruft alle Bewertungen fuer einen Prompt ab.
|
||||
|
||||
### `POST /api/v1/prompts/{id}/reviews`
|
||||
- Erstellt eine neue Bewertung fuer einen Prompt.
|
||||
|
||||
78
KERNMODELL.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Kernmodell
|
||||
|
||||
Dieses Kernmodell beschreibt die wichtigsten Klassen, die OnlyPrompt fuer Login, Profile, Marketplace und Prompt-Kaeufe benoetigt.
|
||||
|
||||
## Klassen
|
||||
|
||||
### User
|
||||
- `id: int`
|
||||
- `username: string`
|
||||
- `email: string`
|
||||
- `passwordHash: string`
|
||||
- `role: string`
|
||||
- `createdAt: datetime`
|
||||
|
||||
### Profile
|
||||
- `id: int`
|
||||
- `userId: int`
|
||||
- `displayName: string`
|
||||
- `bio: string`
|
||||
- `avatarUrl: string`
|
||||
- `specialties: string`
|
||||
|
||||
### Prompt
|
||||
- `id: int`
|
||||
- `creatorId: int`
|
||||
- `categoryId: int`
|
||||
- `title: string`
|
||||
- `description: string`
|
||||
- `content: text`
|
||||
- `price: decimal`
|
||||
- `thumbnailUrl: string`
|
||||
- `ratingAverage: float`
|
||||
- `reviewCount: int`
|
||||
- `status: string`
|
||||
- `createdAt: datetime`
|
||||
- `updatedAt: datetime`
|
||||
|
||||
### Category
|
||||
- `id: int`
|
||||
- `name: string`
|
||||
- `slug: string`
|
||||
|
||||
### Purchase
|
||||
- `id: int`
|
||||
- `buyerId: int`
|
||||
- `promptId: int`
|
||||
- `pricePaid: decimal`
|
||||
- `purchasedAt: datetime`
|
||||
|
||||
### Review
|
||||
- `id: int`
|
||||
- `promptId: int`
|
||||
- `userId: int`
|
||||
- `rating: int`
|
||||
- `comment: string`
|
||||
- `createdAt: datetime`
|
||||
|
||||
## Beziehungen
|
||||
|
||||
- Ein `User` hat genau ein `Profile`.
|
||||
- Ein `User` kann viele `Prompts` erstellen.
|
||||
- Ein `Prompt` gehoert zu genau einer `Category`.
|
||||
- Ein `User` kann viele `Prompts` ueber `Purchase` kaufen.
|
||||
- Ein `User` kann viele `Reviews` schreiben.
|
||||
- Ein `Prompt` kann viele `Reviews` erhalten.
|
||||
|
||||
## UML-Kurzform
|
||||
|
||||
```text
|
||||
User 1 --- 1 Profile
|
||||
User 1 --- * Prompt
|
||||
Category 1 --- * Prompt
|
||||
User 1 --- * Purchase
|
||||
Prompt 1 --- * Purchase
|
||||
User 1 --- * Review
|
||||
Prompt 1 --- * Review
|
||||
```
|
||||
|
||||
1
backend/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
BIN
backend/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/__pycache__/main.cpython-313.pyc
Normal file
BIN
backend/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/__pycache__/schemas.cpython-313.pyc
Normal file
BIN
backend/__pycache__/store.cpython-313.pyc
Normal file
346
backend/main.py
Normal file
@ -0,0 +1,346 @@
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query, Response, status
|
||||
|
||||
from .models import ChatMessage, Favorite, Follow, Prompt, Rating, User
|
||||
from .schemas import (
|
||||
AuthResponse,
|
||||
ChatMessageCreateRequest,
|
||||
ChatMessageResponse,
|
||||
CreatorDetailResponse,
|
||||
FavoriteCreateRequest,
|
||||
FavoriteResponse,
|
||||
FollowResponse,
|
||||
LoginRequest,
|
||||
ProfileUpdateRequest,
|
||||
PromptCreateRequest,
|
||||
PromptResponse,
|
||||
PromptUpdateRequest,
|
||||
RatingCreateRequest,
|
||||
RatingResponse,
|
||||
RegisterRequest,
|
||||
UserResponse,
|
||||
)
|
||||
from .store import store
|
||||
|
||||
app = FastAPI(title="OnlyPrompt API", version="1.0.0")
|
||||
|
||||
|
||||
def get_current_user() -> User:
|
||||
user = store.users.get(store.current_user_id)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="Current user not found.")
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/api/auth/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
|
||||
def register(payload: RegisterRequest) -> AuthResponse:
|
||||
if any(user.email == payload.email for user in store.users.values()):
|
||||
raise HTTPException(status_code=400, detail="Email already exists.")
|
||||
if any(user.username == payload.username for user in store.users.values()):
|
||||
raise HTTPException(status_code=400, detail="Username already exists.")
|
||||
|
||||
now = datetime.utcnow()
|
||||
user_id = store.next_id("user")
|
||||
user = User(
|
||||
id=user_id,
|
||||
email=payload.email,
|
||||
password_hash=payload.password,
|
||||
full_name=payload.full_name,
|
||||
username=payload.username,
|
||||
bio="",
|
||||
location="",
|
||||
avatar_url="",
|
||||
role=payload.role,
|
||||
is_verified=False,
|
||||
created_at=now,
|
||||
)
|
||||
store.users[user_id] = user
|
||||
store.current_user_id = user_id
|
||||
return AuthResponse(message="User created successfully.", user=UserResponse.model_validate(user))
|
||||
|
||||
|
||||
@app.post("/api/auth/login", response_model=AuthResponse)
|
||||
def login(payload: LoginRequest) -> AuthResponse:
|
||||
user = next((item for item in store.users.values() if item.email == payload.email), None)
|
||||
if user is None or user.password_hash != payload.password:
|
||||
raise HTTPException(status_code=401, detail="Invalid email or password.")
|
||||
|
||||
store.current_user_id = user.id
|
||||
return AuthResponse(message="Login successful.", user=UserResponse.model_validate(user))
|
||||
|
||||
|
||||
@app.get("/api/prompts", response_model=list[PromptResponse])
|
||||
def list_prompts(
|
||||
category: str | None = Query(default=None),
|
||||
search: str | None = Query(default=None),
|
||||
) -> list[PromptResponse]:
|
||||
prompts = list(store.prompts.values())
|
||||
|
||||
if category:
|
||||
prompts = [prompt for prompt in prompts if prompt.category.lower() == category.lower()]
|
||||
|
||||
if search:
|
||||
term = search.lower()
|
||||
prompts = [
|
||||
prompt
|
||||
for prompt in prompts
|
||||
if term in prompt.title.lower() or term in prompt.description.lower()
|
||||
]
|
||||
|
||||
return [PromptResponse.model_validate(prompt) for prompt in prompts]
|
||||
|
||||
|
||||
@app.get("/api/prompts/{prompt_id}", response_model=PromptResponse)
|
||||
def get_prompt(prompt_id: int) -> PromptResponse:
|
||||
prompt = store.prompts.get(prompt_id)
|
||||
if prompt is None:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found.")
|
||||
return PromptResponse.model_validate(prompt)
|
||||
|
||||
|
||||
@app.post("/api/prompts", response_model=PromptResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_prompt(payload: PromptCreateRequest) -> PromptResponse:
|
||||
creator = store.users.get(payload.creator_id)
|
||||
if creator is None or creator.role != "creator":
|
||||
raise HTTPException(status_code=404, detail="Creator not found.")
|
||||
|
||||
prompt = Prompt(
|
||||
id=store.next_id("prompt"),
|
||||
title=payload.title,
|
||||
description=payload.description,
|
||||
content=payload.content,
|
||||
image_url=payload.image_url,
|
||||
category=payload.category,
|
||||
price=payload.price,
|
||||
creator_id=payload.creator_id,
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
store.prompts[prompt.id] = prompt
|
||||
return PromptResponse.model_validate(prompt)
|
||||
|
||||
|
||||
@app.put("/api/prompts/{prompt_id}", response_model=PromptResponse)
|
||||
def update_prompt(prompt_id: int, payload: PromptUpdateRequest) -> PromptResponse:
|
||||
prompt = store.prompts.get(prompt_id)
|
||||
if prompt is None:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found.")
|
||||
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
for field, value in updates.items():
|
||||
setattr(prompt, field, value)
|
||||
return PromptResponse.model_validate(prompt)
|
||||
|
||||
|
||||
@app.delete("/api/prompts/{prompt_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_prompt(prompt_id: int) -> Response:
|
||||
if prompt_id not in store.prompts:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found.")
|
||||
del store.prompts[prompt_id]
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@app.get("/api/creators", response_model=list[UserResponse])
|
||||
def list_creators(sort: str | None = Query(default=None)) -> list[UserResponse]:
|
||||
creators = [user for user in store.users.values() if user.role == "creator"]
|
||||
|
||||
if sort == "new":
|
||||
creators.sort(key=lambda item: item.created_at, reverse=True)
|
||||
elif sort == "popular":
|
||||
creators.sort(
|
||||
key=lambda item: sum(1 for follow in store.follows.values() if follow.creator_id == item.id),
|
||||
reverse=True,
|
||||
)
|
||||
elif sort == "top_rated":
|
||||
def creator_rating(user: User) -> float:
|
||||
creator_prompt_ids = [prompt.id for prompt in store.prompts.values() if prompt.creator_id == user.id]
|
||||
ratings = [rating.score for rating in store.ratings.values() if rating.prompt_id in creator_prompt_ids]
|
||||
return sum(ratings) / len(ratings) if ratings else 0
|
||||
|
||||
creators.sort(key=creator_rating, reverse=True)
|
||||
|
||||
return [UserResponse.model_validate(creator) for creator in creators]
|
||||
|
||||
|
||||
@app.get("/api/creators/{creator_id}", response_model=CreatorDetailResponse)
|
||||
def get_creator(creator_id: int) -> CreatorDetailResponse:
|
||||
creator = store.users.get(creator_id)
|
||||
if creator is None or creator.role != "creator":
|
||||
raise HTTPException(status_code=404, detail="Creator not found.")
|
||||
|
||||
prompts = [prompt for prompt in store.prompts.values() if prompt.creator_id == creator_id]
|
||||
return CreatorDetailResponse(
|
||||
creator=UserResponse.model_validate(creator),
|
||||
prompts=[PromptResponse.model_validate(prompt) for prompt in prompts],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/prompts/{prompt_id}/ratings", response_model=list[RatingResponse])
|
||||
def list_ratings(prompt_id: int) -> list[RatingResponse]:
|
||||
if prompt_id not in store.prompts:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found.")
|
||||
ratings = [rating for rating in store.ratings.values() if rating.prompt_id == prompt_id]
|
||||
return [RatingResponse.model_validate(rating) for rating in ratings]
|
||||
|
||||
|
||||
@app.post("/api/prompts/{prompt_id}/ratings", response_model=RatingResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_rating(prompt_id: int, payload: RatingCreateRequest) -> RatingResponse:
|
||||
if prompt_id not in store.prompts:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found.")
|
||||
if payload.user_id not in store.users:
|
||||
raise HTTPException(status_code=404, detail="User not found.")
|
||||
|
||||
rating = Rating(
|
||||
id=store.next_id("rating"),
|
||||
prompt_id=prompt_id,
|
||||
user_id=payload.user_id,
|
||||
score=payload.score,
|
||||
comment=payload.comment,
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
store.ratings[rating.id] = rating
|
||||
return RatingResponse.model_validate(rating)
|
||||
|
||||
|
||||
@app.get("/api/favorites", response_model=list[FavoriteResponse])
|
||||
def list_favorites() -> list[FavoriteResponse]:
|
||||
current_user = get_current_user()
|
||||
favorites = [item for item in store.favorites.values() if item.user_id == current_user.id]
|
||||
return [FavoriteResponse.model_validate(item) for item in favorites]
|
||||
|
||||
|
||||
@app.post("/api/favorites", response_model=FavoriteResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_favorite(payload: FavoriteCreateRequest) -> FavoriteResponse:
|
||||
current_user = get_current_user()
|
||||
if payload.prompt_id not in store.prompts:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found.")
|
||||
|
||||
existing = next(
|
||||
(
|
||||
item
|
||||
for item in store.favorites.values()
|
||||
if item.user_id == current_user.id and item.prompt_id == payload.prompt_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing is not None:
|
||||
return FavoriteResponse.model_validate(existing)
|
||||
|
||||
favorite = Favorite(
|
||||
id=store.next_id("favorite"),
|
||||
user_id=current_user.id,
|
||||
prompt_id=payload.prompt_id,
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
store.favorites[favorite.id] = favorite
|
||||
return FavoriteResponse.model_validate(favorite)
|
||||
|
||||
|
||||
@app.delete("/api/favorites/{prompt_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_favorite(prompt_id: int) -> Response:
|
||||
current_user = get_current_user()
|
||||
favorite_id = next(
|
||||
(
|
||||
item.id
|
||||
for item in store.favorites.values()
|
||||
if item.user_id == current_user.id and item.prompt_id == prompt_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if favorite_id is None:
|
||||
raise HTTPException(status_code=404, detail="Favorite not found.")
|
||||
|
||||
del store.favorites[favorite_id]
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@app.post("/api/follows/{creator_id}", response_model=FollowResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_follow(creator_id: int) -> FollowResponse:
|
||||
current_user = get_current_user()
|
||||
creator = store.users.get(creator_id)
|
||||
if creator is None or creator.role != "creator":
|
||||
raise HTTPException(status_code=404, detail="Creator not found.")
|
||||
|
||||
existing = next(
|
||||
(
|
||||
item
|
||||
for item in store.follows.values()
|
||||
if item.follower_id == current_user.id and item.creator_id == creator_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing is not None:
|
||||
return FollowResponse.model_validate(existing)
|
||||
|
||||
follow = Follow(
|
||||
id=store.next_id("follow"),
|
||||
follower_id=current_user.id,
|
||||
creator_id=creator_id,
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
store.follows[follow.id] = follow
|
||||
return FollowResponse.model_validate(follow)
|
||||
|
||||
|
||||
@app.delete("/api/follows/{creator_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_follow(creator_id: int) -> Response:
|
||||
current_user = get_current_user()
|
||||
follow_id = next(
|
||||
(
|
||||
item.id
|
||||
for item in store.follows.values()
|
||||
if item.follower_id == current_user.id and item.creator_id == creator_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if follow_id is None:
|
||||
raise HTTPException(status_code=404, detail="Follow not found.")
|
||||
|
||||
del store.follows[follow_id]
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@app.get("/api/profile", response_model=UserResponse)
|
||||
def get_profile() -> UserResponse:
|
||||
return UserResponse.model_validate(get_current_user())
|
||||
|
||||
|
||||
@app.put("/api/profile", response_model=UserResponse)
|
||||
def update_profile(payload: ProfileUpdateRequest) -> UserResponse:
|
||||
user = get_current_user()
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
for field, value in updates.items():
|
||||
setattr(user, field, value)
|
||||
return UserResponse.model_validate(user)
|
||||
|
||||
|
||||
@app.get("/api/chats/{user_id}", response_model=list[ChatMessageResponse])
|
||||
def get_chat(user_id: int) -> list[ChatMessageResponse]:
|
||||
current_user = get_current_user()
|
||||
if user_id not in store.users:
|
||||
raise HTTPException(status_code=404, detail="User not found.")
|
||||
|
||||
messages = [
|
||||
message
|
||||
for message in store.chat_messages.values()
|
||||
if {message.sender_id, message.receiver_id} == {current_user.id, user_id}
|
||||
]
|
||||
messages.sort(key=lambda item: item.created_at)
|
||||
return [ChatMessageResponse.model_validate(message) for message in messages]
|
||||
|
||||
|
||||
@app.post("/api/chats", response_model=ChatMessageResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_chat_message(payload: ChatMessageCreateRequest) -> ChatMessageResponse:
|
||||
current_user = get_current_user()
|
||||
if payload.receiver_id not in store.users:
|
||||
raise HTTPException(status_code=404, detail="Receiver not found.")
|
||||
|
||||
message = ChatMessage(
|
||||
id=store.next_id("chat_message"),
|
||||
sender_id=current_user.id,
|
||||
receiver_id=payload.receiver_id,
|
||||
content=payload.content,
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
store.chat_messages[message.id] = message
|
||||
return ChatMessageResponse.model_validate(message)
|
||||
65
backend/models.py
Normal file
@ -0,0 +1,65 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: int
|
||||
email: str
|
||||
password_hash: str
|
||||
full_name: str
|
||||
username: str
|
||||
bio: str
|
||||
location: str
|
||||
avatar_url: str
|
||||
role: str
|
||||
is_verified: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Prompt:
|
||||
id: int
|
||||
title: str
|
||||
description: str
|
||||
content: str
|
||||
image_url: str
|
||||
category: str
|
||||
price: float
|
||||
creator_id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rating:
|
||||
id: int
|
||||
prompt_id: int
|
||||
user_id: int
|
||||
score: int
|
||||
comment: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Favorite:
|
||||
id: int
|
||||
user_id: int
|
||||
prompt_id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Follow:
|
||||
id: int
|
||||
follower_id: int
|
||||
creator_id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatMessage:
|
||||
id: int
|
||||
sender_id: int
|
||||
receiver_id: int
|
||||
content: str
|
||||
created_at: datetime
|
||||
137
backend/schemas.py
Normal file
@ -0,0 +1,137 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
email: EmailStr
|
||||
full_name: str
|
||||
username: str
|
||||
bio: str
|
||||
location: str
|
||||
avatar_url: str
|
||||
role: str
|
||||
is_verified: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=6)
|
||||
full_name: str = Field(min_length=2, max_length=100)
|
||||
username: str = Field(min_length=3, max_length=50)
|
||||
role: str = Field(default="user")
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=6)
|
||||
|
||||
|
||||
class AuthResponse(BaseModel):
|
||||
message: str
|
||||
user: UserResponse
|
||||
|
||||
|
||||
class ProfileUpdateRequest(BaseModel):
|
||||
full_name: Optional[str] = Field(default=None, min_length=2, max_length=100)
|
||||
bio: Optional[str] = Field(default=None, max_length=500)
|
||||
location: Optional[str] = Field(default=None, max_length=120)
|
||||
avatar_url: Optional[str] = Field(default=None, max_length=255)
|
||||
is_verified: Optional[bool] = None
|
||||
|
||||
|
||||
class PromptCreateRequest(BaseModel):
|
||||
title: str = Field(min_length=3, max_length=120)
|
||||
description: str = Field(min_length=10, max_length=500)
|
||||
content: str = Field(min_length=10)
|
||||
image_url: str = Field(max_length=255)
|
||||
category: str = Field(min_length=2, max_length=50)
|
||||
price: float = Field(ge=0)
|
||||
creator_id: int
|
||||
|
||||
|
||||
class PromptUpdateRequest(BaseModel):
|
||||
title: Optional[str] = Field(default=None, min_length=3, max_length=120)
|
||||
description: Optional[str] = Field(default=None, min_length=10, max_length=500)
|
||||
content: Optional[str] = Field(default=None, min_length=10)
|
||||
image_url: Optional[str] = Field(default=None, max_length=255)
|
||||
category: Optional[str] = Field(default=None, min_length=2, max_length=50)
|
||||
price: Optional[float] = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class PromptResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
title: str
|
||||
description: str
|
||||
content: str
|
||||
image_url: str
|
||||
category: str
|
||||
price: float
|
||||
creator_id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class CreatorDetailResponse(BaseModel):
|
||||
creator: UserResponse
|
||||
prompts: list[PromptResponse]
|
||||
|
||||
|
||||
class RatingCreateRequest(BaseModel):
|
||||
user_id: int
|
||||
score: int = Field(ge=1, le=5)
|
||||
comment: str = Field(default="", max_length=500)
|
||||
|
||||
|
||||
class RatingResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
prompt_id: int
|
||||
user_id: int
|
||||
score: int
|
||||
comment: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class FavoriteCreateRequest(BaseModel):
|
||||
prompt_id: int
|
||||
|
||||
|
||||
class FavoriteResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
user_id: int
|
||||
prompt_id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class FollowResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
follower_id: int
|
||||
creator_id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ChatMessageCreateRequest(BaseModel):
|
||||
receiver_id: int
|
||||
content: str = Field(min_length=1, max_length=2000)
|
||||
|
||||
|
||||
class ChatMessageResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
sender_id: int
|
||||
receiver_id: int
|
||||
content: str
|
||||
created_at: datetime
|
||||
103
backend/store.py
Normal file
@ -0,0 +1,103 @@
|
||||
from datetime import datetime
|
||||
|
||||
from .models import ChatMessage, Favorite, Follow, Prompt, Rating, User
|
||||
|
||||
|
||||
class InMemoryStore:
|
||||
def __init__(self) -> None:
|
||||
now = datetime.utcnow()
|
||||
|
||||
self.users = {
|
||||
1: User(
|
||||
id=1,
|
||||
email="jane@example.com",
|
||||
password_hash="demo-password",
|
||||
full_name="Jane Doe",
|
||||
username="janedoe",
|
||||
bio="AI creator focused on writing and coding prompts.",
|
||||
location="Zurich",
|
||||
avatar_url="/images/content/creator1.png",
|
||||
role="creator",
|
||||
is_verified=True,
|
||||
created_at=now,
|
||||
),
|
||||
2: User(
|
||||
id=2,
|
||||
email="max@example.com",
|
||||
password_hash="demo-password",
|
||||
full_name="Max Muster",
|
||||
username="maxm",
|
||||
bio="Prompt buyer and tester.",
|
||||
location="Chur",
|
||||
avatar_url="/images/content/creator2.png",
|
||||
role="user",
|
||||
is_verified=False,
|
||||
created_at=now,
|
||||
),
|
||||
}
|
||||
self.prompts = {
|
||||
1: Prompt(
|
||||
id=1,
|
||||
title="Python Code Assistant",
|
||||
description="Efficiently debug and write Python code with AI assistance.",
|
||||
content="You are an expert Python assistant...",
|
||||
image_url="/images/content/prompt2.png",
|
||||
category="Coding",
|
||||
price=19.99,
|
||||
creator_id=1,
|
||||
created_at=now,
|
||||
)
|
||||
}
|
||||
self.ratings = {
|
||||
1: Rating(
|
||||
id=1,
|
||||
prompt_id=1,
|
||||
user_id=2,
|
||||
score=5,
|
||||
comment="Very useful starter prompt.",
|
||||
created_at=now,
|
||||
)
|
||||
}
|
||||
self.favorites = {
|
||||
1: Favorite(
|
||||
id=1,
|
||||
user_id=2,
|
||||
prompt_id=1,
|
||||
created_at=now,
|
||||
)
|
||||
}
|
||||
self.follows = {
|
||||
1: Follow(
|
||||
id=1,
|
||||
follower_id=2,
|
||||
creator_id=1,
|
||||
created_at=now,
|
||||
)
|
||||
}
|
||||
self.chat_messages = {
|
||||
1: ChatMessage(
|
||||
id=1,
|
||||
sender_id=2,
|
||||
receiver_id=1,
|
||||
content="Hi, I have a question about your prompt.",
|
||||
created_at=now,
|
||||
)
|
||||
}
|
||||
|
||||
self.current_user_id = 2
|
||||
self.next_ids = {
|
||||
"user": 3,
|
||||
"prompt": 2,
|
||||
"rating": 2,
|
||||
"favorite": 2,
|
||||
"follow": 2,
|
||||
"chat_message": 2,
|
||||
}
|
||||
|
||||
def next_id(self, key: str) -> int:
|
||||
value = self.next_ids[key]
|
||||
self.next_ids[key] += 1
|
||||
return value
|
||||
|
||||
|
||||
store = InMemoryStore()
|
||||
19
css/base.css
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
This file contains global base styles and resets
|
||||
--> ensures consistent spacing, font usage, and box sizing
|
||||
across all elements in the application
|
||||
*/
|
||||
|
||||
/* Reset default browser styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Global body styling */
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
170
css/creators.css
Normal file
@ -0,0 +1,170 @@
|
||||
/* Creators page - Discover creators, filter buttons, creator cards */
|
||||
|
||||
/* Full width layout */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.creators-main {
|
||||
background: transparent !important;
|
||||
padding: 20px 32px !important;
|
||||
margin: 0 auto !important;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.creators-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.creators-header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.creators-header p {
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Filter buttons */
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.filter-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border-radius: 30px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
}
|
||||
.filter-btn:hover:not(.active) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Creators grid */
|
||||
.creators-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Creator card */
|
||||
.creator-card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.creator-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
||||
}
|
||||
|
||||
.creator-avatar {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.creator-info {
|
||||
flex: 1;
|
||||
}
|
||||
.creator-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.creator-handle {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.creator-bio {
|
||||
color: #334155;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.creator-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
}
|
||||
.creator-stats i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.follow-btn {
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
padding: 6px 16px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.follow-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.creators-main {
|
||||
padding: 16px !important;
|
||||
}
|
||||
.filter-buttons {
|
||||
gap: 8px;
|
||||
}
|
||||
.filter-btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.creators-main {
|
||||
padding: 12px !important;
|
||||
}
|
||||
.creator-card {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.creator-stats {
|
||||
justify-content: center;
|
||||
}
|
||||
.follow-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
161
css/login.css
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
File contains the styles for the login page
|
||||
--> defines the layout of the login screen
|
||||
*/
|
||||
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex; /* enables flexbox layout for centering */
|
||||
justify-content: center; /* horizontally centers the card */
|
||||
align-items: center; /* vertically centers the card */
|
||||
padding: 24px; /* space inside the page edges */
|
||||
/* Layered background: two soft radial gradients for color accents, then the main background color */
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.12), transparent 35%),
|
||||
radial-gradient(circle at bottom right, rgba(236, 72, 153, 0.10), transparent 30%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
/* Main login card container */
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 430px; /* prevents the card from getting too wide on large screens */
|
||||
background: var(--card); /* uses card color from variables.css */
|
||||
border-radius: 24px; /* rounded corners */
|
||||
box-shadow: var(--shadow); /* soft shadow for card elevation */
|
||||
padding: 40px 32px; /* inner spacing for content */
|
||||
}
|
||||
|
||||
/* Logo area above the form */
|
||||
.login-logo-wrapper {
|
||||
display: flex; /* centers logo horizontally */
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
overflow: hidden; /* hides any part of the logo that overflows the container */
|
||||
}
|
||||
|
||||
/* Full logo styling */
|
||||
.login-logo {
|
||||
width: 100%;
|
||||
max-width: 220px; /* logo never exceeds this width */
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: contain; /* keeps logo aspect ratio, prevents stretching */
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
/* Form layout */
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column; /* stack form fields vertically */
|
||||
gap: 18px; /* vertical space between form groups */
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column; /* label above input */
|
||||
gap: 8px; /* space between label and input */
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 14px 16px; /* vertical and horizontal padding for input */
|
||||
border: 1px solid #dbe2ea; /* subtle border */
|
||||
border-radius: 14px; /* rounded input corners */
|
||||
background: #ffffff;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Highlight input when focused */
|
||||
.form-group input:focus {
|
||||
outline: none; /* removes default browser outline */
|
||||
border-color: #7c3aed; /* purple border on focus */
|
||||
box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.10); /* soft glow for focus state */
|
||||
}
|
||||
|
||||
/* Password field with button inside the same row */
|
||||
.password-wrapper {
|
||||
display: flex; /* input and button in one row */
|
||||
align-items: center; /* vertically center input and button */
|
||||
gap: 10px; /* space between input and show/hide button */
|
||||
}
|
||||
|
||||
.password-wrapper input {
|
||||
flex: 1; /* input takes all available space, button stays compact */
|
||||
}
|
||||
|
||||
.toggle-password {
|
||||
border: none;
|
||||
background: transparent; /* no background for button */
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
cursor: pointer; /* pointer cursor for better UX */
|
||||
}
|
||||
|
||||
/* Main login action button */
|
||||
.login-button {
|
||||
margin-top: 4px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
padding: 14px 18px;
|
||||
background: var(--gradient); /* uses a gradient background from variables */
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.22); /* blue shadow for button depth */
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
opacity: 0.95; /* slight fade on hover for feedback */
|
||||
}
|
||||
|
||||
.signup-text {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
/* used for the 'Don't have an account?' and link below the form */
|
||||
}
|
||||
|
||||
.signup-text a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
/* blue link for sign up/login */
|
||||
}
|
||||
|
||||
/* Smaller spacing and sizing for narrow screens */
|
||||
@media (max-width: 480px) {
|
||||
/* Responsive adjustments for small screens (mobile) */
|
||||
.login-card {
|
||||
padding: 28px 20px; /* less padding on mobile */
|
||||
border-radius: 20px; /* slightly less rounded */
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.7rem; /* smaller title on mobile */
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
max-width: 170px; /* smaller logo on mobile */
|
||||
}
|
||||
}
|
||||
189
css/marketplace.css
Normal file
@ -0,0 +1,189 @@
|
||||
/* Marketplace Page - Prompt cards, filter buttons, full width layout */
|
||||
|
||||
/* Full width layout */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.marketplace-main {
|
||||
background: transparent !important;
|
||||
padding: 20px 32px !important;
|
||||
margin: 0 auto !important;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
/* Header centering */
|
||||
.marketplace-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.marketplace-header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.marketplace-header p {
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Filter buttons - centered */
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.filter-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border-radius: 30px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
}
|
||||
.filter-btn:hover:not(.active) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Prompts grid */
|
||||
.prompts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Prompt card */
|
||||
.prompt-card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.prompt-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
||||
}
|
||||
|
||||
.prompt-img {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.prompt-info {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.prompt-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prompt-author {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.prompt-description {
|
||||
color: #334155;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prompt-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #f59e0b;
|
||||
}
|
||||
.prompt-rating span:first-child i {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.prompt-rating span:last-child {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.prompt-price {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #3b82f6;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.prompt-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.buy-btn, .details-btn {
|
||||
flex: 1;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.buy-btn {
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
}
|
||||
.details-btn {
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
}
|
||||
.buy-btn:hover, .details-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.marketplace-main {
|
||||
padding: 16px !important;
|
||||
}
|
||||
.filter-buttons {
|
||||
gap: 8px;
|
||||
}
|
||||
.filter-btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.marketplace-main {
|
||||
padding: 12px !important;
|
||||
}
|
||||
.prompt-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
94
css/profile.css
Normal file
@ -0,0 +1,94 @@
|
||||
/* Profile Page - Full width layout, darker share button, responsive grid */
|
||||
|
||||
/* Force main content container to full width, remove centering and max-width */
|
||||
.layout > div[style*="flex:1"] {
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Inner spacing for the profile card */
|
||||
.profile-main {
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 20px 32px !important;
|
||||
margin: 0 auto !important;
|
||||
width: 100%;
|
||||
max-width: 1600px; /* Limits content on very large screens, but still wide */
|
||||
}
|
||||
|
||||
/* Make prompts grid use more columns on large screens */
|
||||
.profile-main section:last-child {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
|
||||
gap: 24px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Share button: darker background and text */
|
||||
.profile-header button:last-child {
|
||||
background: #cbd5e1 !important; /* darker gray */
|
||||
color: #1e293b !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Buttons keep rounded corners */
|
||||
.login-button {
|
||||
border-radius: 14px !important;
|
||||
}
|
||||
|
||||
/* Prompt cards: rounded corners */
|
||||
.profile-main section > div {
|
||||
border-radius: 18px !important;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
}
|
||||
|
||||
/* Prompt images: rounded corners */
|
||||
.profile-main section img {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
/* Avatar remains round */
|
||||
.profile-avatar {
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
||||
/* All outer containers stay square */
|
||||
.layout,
|
||||
.profile-main,
|
||||
.profile-header,
|
||||
.profile-tabs,
|
||||
nav {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Responsive: tablets */
|
||||
@media (max-width: 768px) {
|
||||
.profile-main {
|
||||
padding: 16px !important;
|
||||
}
|
||||
.profile-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.profile-header > div:last-child {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive: mobile */
|
||||
@media (max-width: 480px) {
|
||||
.profile-main {
|
||||
padding: 12px !important;
|
||||
}
|
||||
.profile-header > div:last-child {
|
||||
flex-direction: column;
|
||||
}
|
||||
.profile-main section:last-child {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
169
css/sidebar.css
Normal file
@ -0,0 +1,169 @@
|
||||
/*
|
||||
Sidebar styles for OnlyPrompt
|
||||
- modern soft card look
|
||||
- responsive: full sidebar on desktop, icon-only on smaller screens
|
||||
*/
|
||||
|
||||
.sidebar-shell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #eef2f7;
|
||||
padding: 24px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.sidebar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 72px;
|
||||
margin-bottom: 32px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.sidebar-logo-full {
|
||||
max-width: 170px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.sidebar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
text-decoration: none;
|
||||
color: #475569;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
padding: 12px 16px;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.sidebar i {
|
||||
font-size: 1.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-blue {
|
||||
color: #3b82f6 !important;
|
||||
}
|
||||
|
||||
.icon-purple {
|
||||
color: #8b5cf6 !important;
|
||||
}
|
||||
|
||||
.icon-pink {
|
||||
color: #ec4899 !important;
|
||||
}
|
||||
|
||||
/* Active item */
|
||||
.sidebar a.active {
|
||||
background: #eef2ff;
|
||||
color: #2563eb;
|
||||
border-left: 3px solid #3b82f6;
|
||||
}
|
||||
|
||||
/* Bottom logout area */
|
||||
.sidebar-bottom {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.sidebar-logout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-logout:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.logout-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sidebar-logout i {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.logout-arrow {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Responsive: icon-only sidebar */
|
||||
@media (max-width: 900px) {
|
||||
.sidebar-shell {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.sidebar-logo-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-logo-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar .nav-text,
|
||||
.sidebar-logout .nav-text,
|
||||
.logout-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar a,
|
||||
.sidebar-logout {
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sidebar a.active {
|
||||
border-left: none;
|
||||
border-right: 3px solid #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.sidebar a,
|
||||
.sidebar-logout {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
169
css/signup.css
Normal file
@ -0,0 +1,169 @@
|
||||
/*
|
||||
File contains the styles for the signup page
|
||||
--> defines the layout of the signup screen
|
||||
*/
|
||||
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.12), transparent 35%),
|
||||
radial-gradient(circle at bottom right, rgba(236, 72, 153, 0.10), transparent 30%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
/* Main signup card container */
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 430px;
|
||||
background: var(--card); /*variable.css*/
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 40px 32px;
|
||||
}
|
||||
|
||||
/* Logo area above the form */
|
||||
.login-logo-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Full logo styling */
|
||||
.login-logo {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
/* Form layout */
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #dbe2ea;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Highlight input when focused */
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.10);
|
||||
}
|
||||
|
||||
/* Password field with button inside the same row */
|
||||
.password-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.password-wrapper input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toggle-password {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.signup-terms {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
margin: 18px 0 0 0;
|
||||
}
|
||||
.signup-terms a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Main login action button */
|
||||
.login-button {
|
||||
margin-top: 4px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
padding: 14px 18px;
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.22);
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.signup-text {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.signup-text a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Smaller spacing and sizing for narrow screens */
|
||||
@media (max-width: 480px) {
|
||||
.login-card {
|
||||
padding: 28px 20px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
max-width: 170px;
|
||||
}
|
||||
}
|
||||
92
css/topbar.css
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
Topbar styles for OnlyPrompt
|
||||
- clean, modern, full-width
|
||||
- search bar centered (expands on full screen), profile avatar always on the right
|
||||
- ONLY search bar and avatar have rounded corners
|
||||
*/
|
||||
|
||||
.topbar-shell {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
padding: 16px 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.topbar-search {
|
||||
flex: 1; /* Takes all available space */
|
||||
max-width: none; /* No upper limit, expands freely */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 10px 20px;
|
||||
border-radius: 14px; /* Rounded like login inputs */
|
||||
}
|
||||
|
||||
.topbar-search i {
|
||||
color: #94a3b8;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.topbar-search input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 0.95rem;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.topbar-search input::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.topbar-avatar-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.topbar-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 50%; /* Avatar round */
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.topbar-shell {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
.topbar-search {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.topbar-search i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.topbar-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.topbar-shell {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.topbar-search {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
27
css/variables.css
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
This file contains global design variables such as colors,
|
||||
gradients, spacing, and shadows
|
||||
--> these variables ensure a consistent design accross the whole application
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Main gradient used for buttons and highlights */
|
||||
--gradient: linear-gradient(90deg, #7c3aed, #3b82f6, #ec4899);
|
||||
|
||||
/* Background color of the application */
|
||||
--bg: #f8fafc;
|
||||
|
||||
/* Default text color */
|
||||
--text: #0f172a;
|
||||
|
||||
/* Container background */
|
||||
--card: #ffffff;
|
||||
|
||||
/* Border radius for rounded elements */
|
||||
--radius: 16px;
|
||||
|
||||
/* Standard shadow for cards and components */
|
||||
--shadow: 0 10px 30px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
|
||||
158
html/creators3.html
Normal file
@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OnlyPrompt - Discover Creators</title>
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/base.css">
|
||||
<link rel="stylesheet" href="../css/sidebar.css">
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/creators.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex:1; margin:40px auto; max-width:950px;">
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="creators-main">
|
||||
|
||||
<!-- Header / Titel -->
|
||||
<div class="creators-header">
|
||||
<h1>Discover Creators</h1>
|
||||
<p>Follow your favorite prompt artists and get inspired.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filter Buttons -->
|
||||
<div class="filter-buttons">
|
||||
<button class="filter-btn active">Popular</button>
|
||||
<button class="filter-btn">Rising</button>
|
||||
<button class="filter-btn">New</button>
|
||||
<button class="filter-btn">Top Rated</button>
|
||||
</div>
|
||||
|
||||
<!-- Creators Grid -->
|
||||
<div class="creators-grid">
|
||||
|
||||
<!-- Creator Card 1 -->
|
||||
<div class="creator-card">
|
||||
<img src="../images/content/creator1.png" alt="Sarah Jenkins" class="creator-avatar">
|
||||
<div class="creator-info">
|
||||
<h3 class="creator-name">Sarah Jenkins</h3>
|
||||
<div class="creator-handle">@sarahj</div>
|
||||
<p class="creator-bio">AI Explorer | Prompt Curator | Exploring creativity through generative AI.</p>
|
||||
<div class="creator-stats">
|
||||
<span><i class="bi bi-puzzle"></i> 42 prompts</span>
|
||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
||||
</div>
|
||||
<button class="follow-btn">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creator Card 2 -->
|
||||
<div class="creator-card">
|
||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="creator-avatar">
|
||||
<div class="creator-info">
|
||||
<h3 class="creator-name">Alex Chen</h3>
|
||||
<div class="creator-handle">@alexchen</div>
|
||||
<p class="creator-bio">Digital artist & prompt engineer. Creating surreal landscapes.</p>
|
||||
<div class="creator-stats">
|
||||
<span><i class="bi bi-puzzle"></i> 87 prompts</span>
|
||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
||||
</div>
|
||||
<button class="follow-btn">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creator Card 3 -->
|
||||
<div class="creator-card">
|
||||
<img src="../images/content/creator3.png" alt="Mia Wong" class="creator-avatar">
|
||||
<div class="creator-info">
|
||||
<h3 class="creator-name">Mia Wong</h3>
|
||||
<div class="creator-handle">@miawong</div>
|
||||
<p class="creator-bio">Midjourney master | UI/UX prompts | Design systems.</p>
|
||||
<div class="creator-stats">
|
||||
<span><i class="bi bi-puzzle"></i> 124 prompts</span>
|
||||
<span><i class="bi bi-star-fill"></i> 5.0</span>
|
||||
</div>
|
||||
<button class="follow-btn">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creator Card 4 -->
|
||||
<div class="creator-card">
|
||||
<img src="../images/content/creator4.png" alt="Tom Rivera" class="creator-avatar">
|
||||
<div class="creator-info">
|
||||
<h3 class="creator-name">Tom Rivera</h3>
|
||||
<div class="creator-handle">@tomrivera</div>
|
||||
<p class="creator-bio">3D artist | Character design | Sci-fi & fantasy prompts.</p>
|
||||
<div class="creator-stats">
|
||||
<span><i class="bi bi-puzzle"></i> 33 prompts</span>
|
||||
<span><i class="bi bi-star-fill"></i> 4.7</span>
|
||||
</div>
|
||||
<button class="follow-btn">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creator Card 5 -->
|
||||
<div class="creator-card">
|
||||
<img src="../images/content/creator5.png" alt="Emma Watson" class="creator-avatar">
|
||||
<div class="creator-info">
|
||||
<h3 class="creator-name">Emma Watson</h3>
|
||||
<div class="creator-handle">@emmawatson</div>
|
||||
<p class="creator-bio">Watercolor & pet portraits | Whimsical art prompts.</p>
|
||||
<div class="creator-stats">
|
||||
<span><i class="bi bi-puzzle"></i> 56 prompts</span>
|
||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
||||
</div>
|
||||
<button class="follow-btn">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creator Card 6 -->
|
||||
<div class="creator-card">
|
||||
<img src="../images/content/creator6.png" alt="Liam O'Brien" class="creator-avatar">
|
||||
<div class="creator-info">
|
||||
<h3 class="creator-name">Liam O'Brien</h3>
|
||||
<div class="creator-handle">@liamob</div>
|
||||
<p class="creator-bio">Minimalist logo designer | Brand identity prompts.</p>
|
||||
<div class="creator-stats">
|
||||
<span><i class="bi bi-puzzle"></i> 28 prompts</span>
|
||||
<span><i class="bi bi-star-fill"></i> 4.6</span>
|
||||
</div>
|
||||
<button class="follow-btn">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
fetch('../html/sidebar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
// Remove 'active' from all sidebar links
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
// Set 'active' on the third link (Community)
|
||||
const thirdLink = document.querySelectorAll('#sidebar-container .sidebar li a')[2];
|
||||
if (thirdLink) thirdLink.classList.add('active');
|
||||
});
|
||||
|
||||
fetch('../html/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
75
html/login.html
Normal file
@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<!--Info about page but not visible-->
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- For responsive design: adapts width for different devices -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Title shown in browser tab -->
|
||||
<title>OnlyPrompt - Login</title>
|
||||
|
||||
<!-- CSS files for variables, base styles, and login page -->
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/base.css">
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Main container for the login page (CSS layout) -->
|
||||
<main class="login-page">
|
||||
<!-- White login card -->
|
||||
<section class="login-card">
|
||||
<!-- Logo container -->
|
||||
<div class="login-logo-wrapper">
|
||||
<img src="../images/logo_full.png" alt="OnlyPrompt Logo" class="login-logo">
|
||||
</div>
|
||||
|
||||
<h1 class="login-title">Sign In</h1>
|
||||
<p class="login-subtitle">Welcome back! Enter your details below.</p>
|
||||
|
||||
<!-- Login form, id is used for JavaScript validation -->
|
||||
<form id="loginForm" class="login-form">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="yourname@email.com"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<!-- Password field with button to show/hide password -->
|
||||
<div class="password-wrapper">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
>
|
||||
<button type="button" id="togglePassword" class="toggle-password">
|
||||
Show <!-- Click to show/hide password -->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-button">Log In</button>
|
||||
</form>
|
||||
|
||||
<p class="signup-text">
|
||||
Don't have an account?
|
||||
<a href="#">Sign Up</a>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Script for login logic and form validation -->
|
||||
<script src="../js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
185
html/marketplace2.html
Normal file
@ -0,0 +1,185 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OnlyPrompt - Marketplace</title>
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/base.css">
|
||||
<link rel="stylesheet" href="../css/sidebar.css">
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/marketplace.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex:1; margin:40px auto; max-width:950px;">
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="marketplace-main">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="marketplace-header">
|
||||
<h1>Marketplace</h1>
|
||||
<p>Browse and discover high-quality AI prompts</p>
|
||||
</div>
|
||||
|
||||
<!-- Filter Buttons -->
|
||||
<div class="filter-buttons">
|
||||
<button class="filter-btn active">All</button>
|
||||
<button class="filter-btn">Writing</button>
|
||||
<button class="filter-btn">Coding</button>
|
||||
<button class="filter-btn">Art</button>
|
||||
<button class="filter-btn">Marketing</button>
|
||||
<button class="filter-btn">Video</button>
|
||||
<button class="filter-btn">Data</button>
|
||||
</div>
|
||||
|
||||
<!-- Prompts Grid -->
|
||||
<div class="prompts-grid">
|
||||
|
||||
<!-- Prompt Card 1 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/prompt1.jpg" alt="Creative Blog Post Generator" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Creative Blog Post Generator</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Generate engaging blog posts in minutes to captivate your audience.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
||||
<span>(124 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 2 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/prompt2.png" alt="Python Code Assistant" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Python Code Assistant</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Efficiently debug and write Python code with AI assistance.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.7</span>
|
||||
<span>(98 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 3 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/prompt3.jpg" alt="Digital Art Style Guide" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Digital Art Style Guide</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Create stunning digital art styles with this comprehensive guide.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
||||
<span>(156 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 4 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/prompt4.jpg" alt="Marketing Copywriter" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Marketing Copywriter</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Generate engaging marketing copy in minutes.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
||||
<span>(112 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 5 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/prompt5.jpg" alt="Midjourney Image Prompts" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Midjourney Image Prompts</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Curated prompts for stunning Midjourney images.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.7</span>
|
||||
<span>(203 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 6 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/prompt6.jpg" alt="UI Design Assistant" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">UI Design Assistant</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Generate UI components and design systems with AI.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
||||
<span>(87 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
fetch('../html/sidebar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
// Remove 'active' from all sidebar links
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
// Set 'active' on the second link (Marketplace) - index 1
|
||||
const secondLink = document.querySelectorAll('#sidebar-container .sidebar li a')[1];
|
||||
if (secondLink) secondLink.classList.add('active');
|
||||
});
|
||||
|
||||
fetch('../html/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
108
html/profile6.html
Normal file
@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OnlyPrompt - Profile</title>
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/base.css">
|
||||
<link rel="stylesheet" href="../css/sidebar.css">
|
||||
<link rel="stylesheet" href="../css/login.css">
|
||||
<link rel="stylesheet" href="../css/topbar.css">
|
||||
<link rel="stylesheet" href="../css/profile.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div style="flex:1; margin:40px auto; max-width:950px;">
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="login-card profile-main" style="background:#fff;border-radius:18px;box-shadow:0 2px 8px rgba(59,130,246,0.06);padding:24px;">
|
||||
|
||||
<section class="profile-header" style="display:flex;align-items:center;gap:32px;border-bottom:1px solid #e5e7eb;padding-bottom:24px;">
|
||||
|
||||
<img src="../images/content/cat.png" class="profile-avatar" style="width:110px;height:110px;border-radius:50%;object-fit:cover;">
|
||||
|
||||
<div class="profile-info" style="flex:1;">
|
||||
<h1 style="font-size:2rem;font-weight:700;margin-bottom:4px;">Sunny the Cat</h1>
|
||||
<div style="color:#64748b;margin-bottom:8px;">
|
||||
@sunny_the_cat <i class="bi bi-patch-check-fill" style="color:#3b82f6;"></i>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:8px;">
|
||||
🐾 Cat prompt creator | Nap enthusiast | AI Cat Content Curator<br>
|
||||
Chasing lasers and building purrfect prompts.
|
||||
</div>
|
||||
|
||||
<div style="color:#64748b;">Cat City, Dreamland 🐈</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<button class="login-button">Edit Profile</button>
|
||||
<button class="login-button" style="background:#f3f4f6;color:#111;box-shadow:none;">Share Profile</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<nav style="display:flex;gap:24px;border-bottom:2px solid #e5e7eb;margin:32px 0 18px 0;">
|
||||
<a href="#" style="padding:10px 0;color:#3b82f6;font-weight:600;border-bottom:2px solid #3b82f6;">My Prompts (2)</a>
|
||||
<a href="#" style="padding:10px 0;color:#64748b;">Favorites (15)</a>
|
||||
<a href="#" style="padding:10px 0;color:#64748b;">Saved (7)</a>
|
||||
</nav>
|
||||
|
||||
<section style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:24px;">
|
||||
|
||||
<div style="background:#fff;border-radius:18px;box-shadow:0 2px 8px rgba(59,130,246,0.06);padding:18px;display:flex;gap:16px;">
|
||||
<img src="../images/content/post1.png" style="width:60px;height:60px;border-radius:12px;">
|
||||
<div>
|
||||
<div style="font-weight:700;">Galactic Catventure</div>
|
||||
<div style="color:#64748b;margin-bottom:6px;">A cosmic journey of a curious cat exploring the stars.</div>
|
||||
<div style="display:flex;gap:16px;color:#64748b;">
|
||||
<span><i class="bi bi-heart"></i> 128</span>
|
||||
<span><i class="bi bi-chat"></i> 15</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background:#fff;border-radius:18px;box-shadow:0 2px 8px rgba(59,130,246,0.06);padding:18px;display:flex;gap:16px;">
|
||||
<img src="../images/content/post2.png" style="width:60px;height:60px;border-radius:12px;">
|
||||
<div>
|
||||
<div style="font-weight:700;">Minimalist Cat Logo</div>
|
||||
<div style="color:#64748b;margin-bottom:6px;">Sleek logo design for feline fans.</div>
|
||||
<div style="display:flex;gap:16px;color:#64748b;">
|
||||
<span><i class="bi bi-heart"></i> 54</span>
|
||||
<span><i class="bi bi-chat"></i> 6</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
fetch('../html/sidebar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
// Remove 'active' from all sidebar links
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
// Then set 'active' only on the My Profile link
|
||||
const profileLink = document.querySelector('#sidebar-container a[href="profile.html"]');
|
||||
if (profileLink) profileLink.classList.add('active');
|
||||
});
|
||||
|
||||
fetch('../html/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
71
html/sidebar.html
Normal file
@ -0,0 +1,71 @@
|
||||
<!--
|
||||
Reusable sidebar for OnlyPrompt
|
||||
- Load this file dynamically into pages
|
||||
- Add class="active" to the current page link
|
||||
-->
|
||||
|
||||
<div class="sidebar-shell">
|
||||
<!-- Logo -->
|
||||
<div class="sidebar-logo">
|
||||
<img src="../images/logo_full.png" alt="OnlyPrompt Logo" class="sidebar-logo-full">
|
||||
<img src="../images/logo_icon.png" alt="OnlyPrompt Icon" class="sidebar-logo-icon">
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="sidebar">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="dashboard.html" class="active">
|
||||
<i class="bi bi-house icon-blue"></i>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="marketplace.html">
|
||||
<i class="bi bi-shop icon-purple"></i>
|
||||
<span class="nav-text">Marketplace</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="community.html">
|
||||
<i class="bi bi-people icon-pink"></i>
|
||||
<span class="nav-text">Community</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="chats.html">
|
||||
<i class="bi bi-chat-dots icon-blue"></i>
|
||||
<span class="nav-text">Chats</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="settings.html">
|
||||
<i class="bi bi-gear icon-purple"></i>
|
||||
<span class="nav-text">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="profile.html">
|
||||
<i class="bi bi-person icon-pink"></i>
|
||||
<span class="nav-text">My Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Logout bottom -->
|
||||
<div class="sidebar-bottom">
|
||||
<a href="login.html" class="sidebar-logout">
|
||||
<div class="logout-left">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<span class="nav-text">Logout</span>
|
||||
</div>
|
||||
<i class="bi bi-chevron-right logout-arrow"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
105
html/signup.html
Normal file
@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<!--Info about page but not visible-->
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- For responsive design: adapts width for different devices -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Title shown in browser tab -->
|
||||
<title>OnlyPrompt - Login</title>
|
||||
|
||||
<!-- CSS files for variables, base styles, and login page -->
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/base.css">
|
||||
<link rel="stylesheet" href="../css/signup.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Main container for the login page (CSS layout) -->
|
||||
<main class="login-page">
|
||||
<!-- White login card -->
|
||||
<section class="login-card">
|
||||
<!-- Logo container -->
|
||||
<div class="login-logo-wrapper">
|
||||
<img src="../images/logo_full.png" alt="OnlyPrompt Logo" class="login-logo">
|
||||
</div>
|
||||
|
||||
<h1 class="login-title">Sign Up</h1>
|
||||
<p class="login-subtitle">Create your account to get started.</p>
|
||||
|
||||
<!-- Login form, id is used for JavaScript validation -->
|
||||
<form id="loginForm" class="login-form">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="yourname@email.com"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<!-- Password field with button to show/hide password -->
|
||||
<div class="password-wrapper">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
>
|
||||
<button type="button" id="togglePassword" class="toggle-password">
|
||||
Show <!-- Click to show/hide password -->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="fullName"
|
||||
name="fullName"
|
||||
placeholder="Enter your full name"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<p class="signup-terms">
|
||||
By signing up, you agree to our
|
||||
<a href="#">Terms</a>,
|
||||
<a href="#">Privacy Policy</a> and
|
||||
<a href="#">Cookies Policy</a>.
|
||||
</p>
|
||||
|
||||
|
||||
<button type="submit" class="login-button">Sign Up</button>
|
||||
</form>
|
||||
|
||||
<p class="signup-text">
|
||||
Have an account?
|
||||
<a href="#">Log In</a>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Script for login logic and form validation -->
|
||||
<script src="../js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
19
html/topbar.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!--
|
||||
Reusable topbar for OnlyPrompt
|
||||
- Search in the middle
|
||||
- small action icons
|
||||
- profile avatar on the right
|
||||
-->
|
||||
|
||||
<header class="topbar-shell">
|
||||
<div class="topbar-search">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" placeholder="Search">
|
||||
</div>
|
||||
|
||||
<!-- Profile avatar on the right (must be changed with backend) -->
|
||||
<button class="topbar-avatar-btn" aria-label="Profile">
|
||||
<img src="../images/content/cat.png" alt="Profile Picture" class="topbar-avatar">
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
BIN
images/content/cat.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
images/content/creator1.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
images/content/creator2.png
Normal file
|
After Width: | Height: | Size: 579 KiB |
BIN
images/content/creator4.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
images/content/post1.png
Normal file
|
After Width: | Height: | Size: 868 KiB |
BIN
images/content/post2.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
images/content/prompt2.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
images/logo_full.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
images/logo_icon.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
images/logo_text.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
email-validator
|
||||