Compare commits

...

5 Commits

Author SHA1 Message Date
Thuvaraka Yogarajah
4f34251198 Add initial API backend scaffold 2026-04-06 22:34:22 +02:00
da52852fdf removed files 2026-04-06 16:32:00 +02:00
7b23b296d3 marketplace 2026-04-06 16:30:55 +02:00
a4a5d03f9f creators page 2026-04-06 16:21:33 +02:00
11e973ce61 login, signup, profile page setup 2026-04-05 18:04:51 +02:00
39 changed files with 2620 additions and 0 deletions

75
API_ENDPOINTS.md Normal file
View 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
View 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
View File

@ -0,0 +1 @@

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

346
backend/main.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/content/creator1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
images/content/creator2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

BIN
images/content/creator4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
images/content/post1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

BIN
images/content/post2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
images/content/prompt2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
images/logo_full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
images/logo_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
images/logo_text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
fastapi
uvicorn
pydantic
email-validator