(UI): Implemented much of the frontend recipe creation wizard. #5
@ -1 +1,5 @@
|
||||
# Potion: Recipe Sharing Platform
|
||||
|
||||
## Todo List
|
||||
|
||||
- [-] Ingrident lists/sections
|
||||
|
||||
@ -239,19 +239,19 @@ also have a list of attributes which are to be implemented at the database level
|
||||
data fields will also have a small example object. A more in-depth data structure can be
|
||||
found in **OTHER** section.
|
||||
|
||||
- [ ] Recipes: Represents a single recipe.
|
||||
- [ ] ID (PK) Serial
|
||||
- [ ] Title (Unique, Required) string(128)
|
||||
- [ ] Description (Required) text
|
||||
- [ ] Instructions (Required) string(1024)[]
|
||||
- [ ] Serves (Required) int(0..16)
|
||||
- [ ] Difficulty (Required) int(1..5)
|
||||
- [ ] Duration (Required) JSONB({ "total": int, "prep": int, "cook": int })
|
||||
- [ ] Category (Required) E_Meal (defined in the [enums](#enums-and-types) section)
|
||||
- [ ] Ingredients (Required) JSONB({ "item_a": [{ "name": string, "quantity": string }], "item_b": ... })
|
||||
- [ ] UserId (FK: User.Id) Serial
|
||||
- [ ] Modified () date/time stamp
|
||||
- [ ] Created (Required) date/time stamp
|
||||
- [x] Recipes: Represents a single recipe.
|
||||
- [x] ID (PK) Serial
|
||||
- [x] Title (Required) string(128)
|
||||
- [x] Description (Required) text
|
||||
- [x] Instructions (Required) string(1024)[]
|
||||
- [x] Serves (Required) int(0..16)
|
||||
- [x] Difficulty (Required) int(1..5)
|
||||
- [x] Duration (Required) JSONB({ "total": int, "prep": int, "cook": int })
|
||||
- [x] Category (Required) E_Meal (defined in the [enums](#enums-and-types) section)
|
||||
- [x] Ingredients (Required) JSONB({ "item_a": [{ "name": string, "quantity": string }], "item_b": ... })
|
||||
- [x] UserId (FK: User.Id) Serial
|
||||
- [x] Modified () date/time stamp
|
||||
- [x] Created (Required) date/time stamp
|
||||
|
||||
- [x] Users: Represents a single user.
|
||||
- [x] ID (PK) Serial
|
||||
@ -325,14 +325,14 @@ found in **OTHER** section.
|
||||
Below is a breakdown of the required enumerated types that should be stored in the database.
|
||||
Various tables will reference these types.
|
||||
|
||||
- [ ] E_Meal: Type to represent the type of meal of a recipe.
|
||||
- [ ] breakfast: string
|
||||
- [ ] lunch: string
|
||||
- [ ] dinner: string
|
||||
- [ ] desert: string
|
||||
- [ ] snack: string
|
||||
- [ ] side: string
|
||||
- [ ] other: string
|
||||
- [x] E_Meal: Type to represent the type of meal of a recipe.
|
||||
- [x] breakfast: string
|
||||
- [x] lunch: string
|
||||
- [x] dinner: string
|
||||
- [x] dessert: string
|
||||
- [x] snack: string
|
||||
- [x] side: string
|
||||
- [x] other: string
|
||||
|
||||
- [ ] E_Notification: Type to represent a type of user notification.
|
||||
- [ ] comment: string
|
||||
|
||||
49
internal/app/handlers/recipe_handler.go
Normal file
49
internal/app/handlers/recipe_handler.go
Normal file
@ -0,0 +1,49 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
// domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
)
|
||||
|
||||
func CreateRecipe(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
|
||||
title := ctx.PostForm("title")
|
||||
description := ctx.PostForm("description")
|
||||
preparation := ctx.PostForm("preparation-time")
|
||||
cook := ctx.PostForm("cook-time")
|
||||
serving := ctx.PostForm("serving-size")
|
||||
category := ctx.PostForm("category")
|
||||
difficulty := ctx.PostForm("difficulty")
|
||||
ingredients := ctx.PostFormArray("ingredients")
|
||||
quantity := ctx.PostFormArray("quantity")
|
||||
instructions := ctx.PostFormArray("instructions")
|
||||
tags := ctx.PostForm("tags") // this is a list of strings split with a comma (,)
|
||||
|
||||
// Have to get the image differently
|
||||
image, err := ctx.FormFile("image")
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"title": title,
|
||||
"description": description,
|
||||
"cook time": cook,
|
||||
"preparation time": preparation,
|
||||
"serving size": serving,
|
||||
"category": category,
|
||||
"difficulty": difficulty,
|
||||
"ingredients": ingredients,
|
||||
"quantity": quantity,
|
||||
"instructions": instructions,
|
||||
"tags": tags,
|
||||
"image": image.Filename,
|
||||
})
|
||||
|
||||
// deps.RecipeService.CreateRecipe(ctx)
|
||||
// ctx.JSON(http.StatusCreated, gin.H{"recipe": recipe})
|
||||
}
|
||||
75
internal/app/handlers/state_handler.go
Normal file
75
internal/app/handlers/state_handler.go
Normal file
@ -0,0 +1,75 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const TAG_HTML = `
|
||||
<li
|
||||
hx-post="/v1/web/state/tags/delete"
|
||||
hx-trigger="click"
|
||||
hx-target="#tag-list"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="#tag-list"
|
||||
hx-vals='{"target": "%s"}'
|
||||
class="flex text-xs items-center bg-blue-100 w-fit px-2 py-1 rounded-full gap-x-1 select-none cursor-pointer hover:bg-blue-200 duration-300">
|
||||
× %s
|
||||
</li>
|
||||
`
|
||||
|
||||
const TAG_LIST_HTML = `
|
||||
<input
|
||||
hx-swap-oob="outerHTML"
|
||||
type="hidden"
|
||||
name="tags"
|
||||
id="tags"
|
||||
value="%s"
|
||||
/>
|
||||
`
|
||||
|
||||
func NewTag(ctx *gin.Context) {
|
||||
tag := strings.ToLower(ctx.PostForm("tag"))
|
||||
tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||
|
||||
tags = append([]string{tag}, tags...)
|
||||
|
||||
var html string
|
||||
var cleaned_tags []string
|
||||
for _, tag := range tags {
|
||||
if tag != "" {
|
||||
html += fmt.Sprintf(TAG_HTML, tag, tag)
|
||||
|
||||
// Ensure that the list provided does not contain blank spaces.
|
||||
// This is another measure to ensure this state is bulletproof.
|
||||
cleaned_tags = append(cleaned_tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute OOB swap for the tags
|
||||
html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(cleaned_tags, ","))
|
||||
|
||||
ctx.String(http.StatusOK, html)
|
||||
}
|
||||
|
||||
func DeleteTag(ctx *gin.Context) {
|
||||
tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||
target := ctx.PostForm("target")
|
||||
|
||||
var html string
|
||||
var new_tags []string
|
||||
for _, tag := range tags {
|
||||
if tag != target && tag != "" {
|
||||
html += fmt.Sprintf(TAG_HTML, tag, tag)
|
||||
new_tags = append(new_tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute OOB swap for the tags
|
||||
html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(new_tags, ","))
|
||||
|
||||
ctx.String(http.StatusOK, html)
|
||||
}
|
||||
@ -113,12 +113,15 @@ func (s *Server) Setup() *Server {
|
||||
|
||||
// Initialize and inject dependencies
|
||||
userRepo := repository.NewUserRepository(s.DB)
|
||||
recipeRepo := repository.NewRecipeRepository(s.DB)
|
||||
userService := service.NewUserService(userRepo)
|
||||
authService := service.NewAuthService(userRepo, jwtSecret)
|
||||
recipeService := service.NewRecipeService(recipeRepo)
|
||||
|
||||
deps := &domain.InjectedDependencies{
|
||||
UserService: userService,
|
||||
AuthService: authService,
|
||||
UserService: userService,
|
||||
AuthService: authService,
|
||||
RecipeService: recipeService,
|
||||
}
|
||||
|
||||
// Apply middleware
|
||||
@ -134,6 +137,7 @@ func (s *Server) Setup() *Server {
|
||||
// Domain specific routers
|
||||
router_web := router_v1.Group(domain.WEB)
|
||||
router_api := router_v1.Group(domain.API)
|
||||
router_state := router_web.Group("state")
|
||||
|
||||
// Static routes
|
||||
router_web.Static("/static", "./web/static")
|
||||
@ -161,10 +165,18 @@ func (s *Server) Setup() *Server {
|
||||
router_web.GET("/profile", handlers.ProfilePage)
|
||||
router_web.GET("/list", handlers.ListPage)
|
||||
|
||||
// WEB state endpoints
|
||||
router_state.POST("/tags", handlers.NewTag)
|
||||
router_state.POST("/tags/delete", handlers.DeleteTag)
|
||||
|
||||
// Authentication
|
||||
router_api.GET("/auth/login", handlers.GoogleLogin)
|
||||
router_api.GET("/auth/callback", handlers.GoogleCallback)
|
||||
router_api.GET("/auth/logout", handlers.Logout)
|
||||
|
||||
// Recipe endpoints
|
||||
// TODO: This should be post. Temp!
|
||||
router_api.POST("/recipe", handlers.CreateRecipe)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
66
internal/app/service/recipe_service.go
Normal file
66
internal/app/service/recipe_service.go
Normal file
@ -0,0 +1,66 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
)
|
||||
|
||||
// RecipeService implements the domain.RecipeService defined in the domain module.
|
||||
type RecipeService struct {
|
||||
recipeRepository domain.RecipeRepository
|
||||
}
|
||||
|
||||
// Compile-time check to ensure the RecipeService implements domain.RecipeService
|
||||
var _ domain.RecipeService = (*RecipeService)(nil)
|
||||
|
||||
// NewRecipeService creates a user service object which can be passed into the context. The service
|
||||
// requires a recipe repository which it will use to hit the database when needed.
|
||||
func NewRecipeService(recipeRepository domain.RecipeRepository) domain.RecipeService {
|
||||
return &RecipeService{recipeRepository: recipeRepository}
|
||||
}
|
||||
|
||||
func (s *RecipeService) CreateRecipe(ctx *gin.Context) domain.Recipe {
|
||||
// TODO: Implement
|
||||
|
||||
recipe := domain.Recipe{
|
||||
Title: "Delicious Go Curry",
|
||||
Description: "A savory and easy-to-make curry, perfect for weeknights.",
|
||||
Instructions: []string{
|
||||
"Chop all vegetables.",
|
||||
"Sauté onions and garlic until fragrant.",
|
||||
"Add curry paste and stir for 1 minute.",
|
||||
"Add coconut milk and vegetables, simmer until cooked.",
|
||||
"Serve with rice.",
|
||||
},
|
||||
Serves: 4,
|
||||
Difficulty: 3,
|
||||
Duration: domain.RecipeDuration{
|
||||
Total: 45,
|
||||
Prep: 15,
|
||||
Cook: 30,
|
||||
},
|
||||
Category: domain.MealDinner, // Using our EMeal type. Ensure this matches an updated enum value.
|
||||
Ingredients: []domain.RecipeIngredient{
|
||||
{Name: "Onion", Quantity: "1 large"},
|
||||
{Name: "Garlic", Quantity: "3 cloves"},
|
||||
{Name: "Curry Paste", Quantity: "2 tbsp"},
|
||||
{Name: "Coconut Milk", Quantity: "400ml can"},
|
||||
{Name: "Broccoli", Quantity: "1 head"},
|
||||
{Name: "Bell Pepper", Quantity: "1 red"},
|
||||
{Name: "Rice", Quantity: "As needed"},
|
||||
},
|
||||
UserId: 3,
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"err": err.Error()})
|
||||
return domain.Recipe{}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, gin.H{"recipe": recipe})
|
||||
return recipe
|
||||
}
|
||||
49
internal/domain/recipe/recipe.go
Normal file
49
internal/domain/recipe/recipe.go
Normal file
@ -0,0 +1,49 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// RecipeDuration is the duration to prepare recipe. It has JSON tags which allows it to be
|
||||
// marshaled into a JSON object and stored in the database (JSONB).
|
||||
type RecipeDuration struct {
|
||||
Total int `json:"total"`
|
||||
Prep int `json:"prep"`
|
||||
Cook int `json:"cook"`
|
||||
}
|
||||
|
||||
// RecipeMeal is the database enum E_MEAL which defines the meal type of a recipe. Postgres enums
|
||||
// are case sensitive so these must match the values in the database exactly.
|
||||
type RecipeMeal string
|
||||
|
||||
const (
|
||||
MealBreakfast RecipeMeal = "breakfast"
|
||||
MealLunch RecipeMeal = "lunch"
|
||||
MealDinner RecipeMeal = "dinner"
|
||||
MealDessert RecipeMeal = "dessert"
|
||||
MealSnack RecipeMeal = "snack"
|
||||
MealSide RecipeMeal = "side"
|
||||
MealOther RecipeMeal = "other"
|
||||
)
|
||||
|
||||
// RecipeIngredient is a single ingredients in a recipe. These have JSON tags which allow them
|
||||
// to be marshaled into a JSON array and stored in the database (JSONB).
|
||||
type RecipeIngredient struct {
|
||||
Name string `json:"name"`
|
||||
Quantity string `json:"quantity"`
|
||||
}
|
||||
|
||||
// Recipe is the database model of a recipe. There is no need to map to a different model so
|
||||
// this will remain in the domain.
|
||||
type Recipe struct {
|
||||
Id int
|
||||
Title string
|
||||
Description string
|
||||
Instructions []string
|
||||
Serves int
|
||||
Difficulty int
|
||||
Duration RecipeDuration
|
||||
Category RecipeMeal
|
||||
Ingredients []RecipeIngredient // Just a list of ingredients
|
||||
UserId int
|
||||
Modified *time.Time // Pointer to allow null
|
||||
Created time.Time
|
||||
}
|
||||
6
internal/domain/recipe/repository.go
Normal file
6
internal/domain/recipe/repository.go
Normal file
@ -0,0 +1,6 @@
|
||||
package domain
|
||||
|
||||
type RecipeRepository interface {
|
||||
// TODO: Not sure the input type yet
|
||||
CreateRecipe(recipe *Recipe) error
|
||||
}
|
||||
8
internal/domain/recipe/service.go
Normal file
8
internal/domain/recipe/service.go
Normal file
@ -0,0 +1,8 @@
|
||||
package domain
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type RecipeService interface {
|
||||
CreateRecipe(ctx *gin.Context) Recipe
|
||||
}
|
||||
|
||||
@ -4,12 +4,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
||||
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
)
|
||||
|
||||
type InjectedDependencies struct {
|
||||
UserService domainUser.UserService
|
||||
AuthService domainAuth.AuthService
|
||||
UserService domainUser.UserService
|
||||
AuthService domainAuth.AuthService
|
||||
RecipeService domainRecipe.RecipeService
|
||||
}
|
||||
|
||||
type JwtClaims struct {
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
||||
-- Desc: Create the E_MEAL enum.
|
||||
-- Date: 06/25/2025
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE E_MEAL AS ENUM(
|
||||
'breakfast',
|
||||
'lunch',
|
||||
'dinner',
|
||||
'dessert',
|
||||
'snack',
|
||||
'side',
|
||||
'other'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,23 @@
|
||||
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
||||
-- Desc: Create the recipes table in the database.
|
||||
-- Date: 06/25/2025
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Create the recipes table
|
||||
CREATE TABLE IF NOT EXISTS Recipes (
|
||||
Id SERIAL PRIMARY KEY NOT NULL,
|
||||
Title VARCHAR(128) NOT NULL,
|
||||
Description TEXT NOT NULL,
|
||||
Instructions VARCHAR(1024)[] NOT NULL,
|
||||
Serves INTEGER NOT NULL CHECK (serves >= 0 AND serves <= 16),
|
||||
Difficulty INTEGER NOT NULL CHECK (difficulty >= 1 AND difficulty <= 5),
|
||||
Duration JSONB NOT NULL,
|
||||
Category E_MEAL NOT NULL,
|
||||
Ingredients JSONB NOT NULL,
|
||||
UserId INTEGER NOT NULL REFERENCES users(id),
|
||||
Modified TIMESTAMPTZ,
|
||||
Created TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,83 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type RecipeRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Compile-time check to ensure the RecipeRepository implements domain.RecipeRepository
|
||||
var _ domain.RecipeRepository = (*RecipeRepository)(nil)
|
||||
|
||||
// NewRecipeRepository creates a user repository object which is used by the user service to access
|
||||
// the database. Any recipe related database operations will take place in this repository.
|
||||
func NewRecipeRepository(db *sql.DB) domain.RecipeRepository {
|
||||
return &RecipeRepository{db: db}
|
||||
}
|
||||
|
||||
// NOTE: This function modified the provided recipe with the new values, such as id and time stamp
|
||||
func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
query := `INSERT INTO recipes (
|
||||
title, description, instructions, serves, difficulty,
|
||||
duration, category, ingredients, userid, modified, created
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
|
||||
) RETURNING id;`
|
||||
|
||||
// NOTE: Data steps
|
||||
// cast duration to JSON
|
||||
// cast ingredients to JSON
|
||||
// cast category to string
|
||||
// use nil for the modified time
|
||||
|
||||
durationJSON, err := json.Marshal(recipe.Duration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ingredientsJSON, err := json.Marshal(recipe.Ingredients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var id int
|
||||
if err = tx.QueryRow(
|
||||
query,
|
||||
recipe.Title,
|
||||
recipe.Description,
|
||||
pq.Array(recipe.Instructions),
|
||||
recipe.Serves,
|
||||
recipe.Difficulty,
|
||||
durationJSON,
|
||||
string(recipe.Category),
|
||||
ingredientsJSON,
|
||||
recipe.UserId,
|
||||
nil,
|
||||
recipe.Created,
|
||||
).Scan(&id); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the new ID
|
||||
recipe.Id = id
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -56,6 +56,7 @@ templ FilterDropdown() {
|
||||
Difficulty
|
||||
</h3>
|
||||
<div class="flex text-xs flex-wrap gap-1">
|
||||
@dropdownButton("Beginner")
|
||||
@dropdownButton("Easy")
|
||||
@dropdownButton("Intermediate")
|
||||
@dropdownButton("Challegening")
|
||||
|
||||
@ -131,6 +131,10 @@ func FilterDropdown() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = dropdownButton("Beginner").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = dropdownButton("Easy").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
|
||||
@ -3,5 +3,282 @@ package templates
|
||||
import "github.com/haydenhargreaves/Potion/internal/templates/components"
|
||||
|
||||
templ CreatePage() {
|
||||
@components.Navbar("create")
|
||||
@components.Navbar("create")
|
||||
<div class="w-full h-fit flex justify-center">
|
||||
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
|
||||
@Page()
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Page() {
|
||||
@components.BannerText("Create Your Masterpiece")
|
||||
<p id="output"></p>
|
||||
<div class="mx-4 md:mx-16 my-8">
|
||||
<p class="mb-8">
|
||||
Welcome to the Recipe Creation Wizard! Simply fill in the details about your culinary creation,
|
||||
including the recipe's name, a description, and other specifics like its category, duration,
|
||||
and difficulty. Don't forget to dynamically add all your ingredients and instructions using
|
||||
the dedicated buttons, and feel free to upload an appealing image. All required fields are
|
||||
marked with an <span class="text-red-500">*</span>. Once everything looks perfect, just hit the "Create Recipe"
|
||||
button to
|
||||
share your masterpiece!
|
||||
</p>
|
||||
<form>
|
||||
<div class="flex flex-col">
|
||||
<label for="title" class="text-sm mb-2">
|
||||
Recipe Title
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
placeholder="e.g., Classic Chicken Curry"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col my-4">
|
||||
<label for="description" class="text-sm mb-2">
|
||||
Description
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
placeholder="A brief description of your delicious recipe..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="my-4 flex flex-col gap-x-2">
|
||||
<div class="flex flex-col flex-grow">
|
||||
<label for="tags" class="text-sm mb-2">
|
||||
Recipe Tags
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
hx-post="/v1/web/state/tags"
|
||||
maxlength="32"
|
||||
hx-trigger="keyup[keyCode==13]"
|
||||
hx-on::after-request="this.value=''"
|
||||
hx-swap="innerHTML"
|
||||
hx-target="#tag-list"
|
||||
enterkeyhint="done"
|
||||
type="text"
|
||||
id="tag"
|
||||
name="tag"
|
||||
placeholder="e.g., Healthy"
|
||||
/>
|
||||
<input type="hidden" name="tags" id="tags" value=""/>
|
||||
</div>
|
||||
<ul id="tag-list" class="my-2 flex gap-1 flex-wrap"></ul>
|
||||
</div>
|
||||
<div class="my-4 flex gap-x-2">
|
||||
<div class="flex flex-col flex-grow w-1/3">
|
||||
<label for="preparation-time" class="text-sm mb-2">
|
||||
Prep Time
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="number"
|
||||
id="preparation-time"
|
||||
name="preparation-time"
|
||||
placeholder="e.g., 20"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col flex-grow w-1/3">
|
||||
<label for="cook-time" class="text-sm mb-2">
|
||||
Cook Time
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="number"
|
||||
id="cook-time"
|
||||
name="cook-time"
|
||||
placeholder="e.g., 45"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col flex-grow w-1/3">
|
||||
<label for="serving-size" class="text-sm mb-2">
|
||||
Serving Size
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="number"
|
||||
max="16"
|
||||
min="1"
|
||||
id="serving-size"
|
||||
name="serving-size"
|
||||
placeholder="e.g., 4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4 flex gap-x-2">
|
||||
<div class="flex flex-col flex-grow w-1/3">
|
||||
<label for="category" class="text-sm mb-2">
|
||||
Category
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="category"
|
||||
name="category"
|
||||
class="border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
>
|
||||
<option value="">Select a category</option>
|
||||
<option value="breakfast">Breakfast</option>
|
||||
<option value="lunch">Lunch</option>
|
||||
<option value="dinner">Dinner</option>
|
||||
<option value="dessert">Dessert</option>
|
||||
<option value="snack">Snack</option>
|
||||
<option value="side">Side</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col flex-grow w-1/3">
|
||||
<label for="difficulty" class="text-sm mb-2">
|
||||
Difficulty
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="difficulty"
|
||||
name="difficulty"
|
||||
class="border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
>
|
||||
<option value="">Select a difficulty</option>
|
||||
<option value="1">Beginner</option>
|
||||
<option value="2">Easy</option>
|
||||
<option value="3">Intermediate</option>
|
||||
<option value="4">Challenging</option>
|
||||
<option value="5">Extreme</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col my-4">
|
||||
<label for="ingredients" class="text-sm">
|
||||
Ingredients
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div id="ingredient-list">
|
||||
<div class="w-full flex gap-x-2 py-2">
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="flex-grow border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="text"
|
||||
id="ingredients"
|
||||
name="ingredients"
|
||||
placeholder="Ingredient name (e.g., Chicken Breast)"
|
||||
/>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="w-1/3 border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="text"
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
placeholder="Quantity (e.g., 1lb)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick="addIngredient();"
|
||||
class="text-base md:text-lg text-white bg-blue-500 w-fit px-5 py-2 rounded-lg cursor-pointer"
|
||||
>
|
||||
Add Ingredient
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col my-4">
|
||||
<label for="instructions" class="text-sm">
|
||||
Instructions
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div id="instruction-list" class="flex flex-col">
|
||||
<textarea
|
||||
class="border border-gray-300 my-2 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm"
|
||||
id="instructions"
|
||||
name="instructions"
|
||||
rows="3"
|
||||
placeholder="Step 1: Describe this step..."
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick="addInstruction();"
|
||||
class="text-base md:text-lg text-white bg-blue-500 w-fit px-5 py-2 rounded-lg cursor-pointer"
|
||||
>
|
||||
Add Instruction Step
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col my-4">
|
||||
<label for="image" class="text-sm">
|
||||
Recipe Image
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
name="image"
|
||||
id="image"
|
||||
class="my-2 block w-full text-sm text-placeholder file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:bg-blue-100 file:text-blue-700 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<!-- <form hx-post="/v1/api/recipe" hx-swap="innerHTML" hx-target="#output" hx-trigger="submit"> -->
|
||||
<button
|
||||
type="submit"
|
||||
hx-post="/v1/api/recipe"
|
||||
hx-swap="innerHTML"
|
||||
hx-target="#output"
|
||||
hx-trigger="click"
|
||||
hx-encoding="multipart/form-data"
|
||||
class="w-full mt-8 bg-gradient-to-r from-blue-200 to-purple-200 py-2 rounded-lg text-lg cursor-pointer shadow-md"
|
||||
>
|
||||
Create Recipe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
function addIngredient() {
|
||||
const list = document.getElementById("ingredient-list");
|
||||
const item = document.createElement("div");
|
||||
item.classList.add("w-full", "flex", "gap-x-2", "py-2");
|
||||
item.innerHTML = `
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="flex-grow border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="text"
|
||||
id="ingredients"
|
||||
name="ingredients"
|
||||
placeholder="Ingredient name (e.g., Chicken Breast)"
|
||||
/>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="w-1/3 border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="text"
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
placeholder="Quantity (e.g., 1lb)"
|
||||
/>
|
||||
`;
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
function addInstruction() {
|
||||
const list = document.getElementById("instruction-list");
|
||||
const itemNum = list.children.length + 1;
|
||||
const item = document.createElement("textarea");
|
||||
item.id = "instructions";
|
||||
item.name = "instructions";
|
||||
item.className = "border border-gray-300 my-2 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm";
|
||||
item.rows = "3";
|
||||
item.placeholder = `Step ${itemNum}: Describe this step...`;
|
||||
list.appendChild(item);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -8,8 +8,6 @@
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
--color-red-100: oklch(93.6% 0.032 17.717);
|
||||
--color-red-200: oklch(88.5% 0.062 18.334);
|
||||
--color-red-400: oklch(70.4% 0.191 22.216);
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
||||
--color-blue-200: oklch(88.2% 0.059 254.128);
|
||||
@ -19,6 +17,7 @@
|
||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||
--color-purple-100: oklch(94.6% 0.033 307.174);
|
||||
--color-purple-200: oklch(90.2% 0.063 306.703);
|
||||
--color-gray-50: oklch(98.5% 0.002 247.839);
|
||||
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||
@ -35,6 +34,8 @@
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
--text-sm: 0.875rem;
|
||||
--text-sm--line-height: calc(1.25 / 0.875);
|
||||
--text-base: 1rem;
|
||||
--text-base--line-height: calc(1.5 / 1);
|
||||
--text-lg: 1.125rem;
|
||||
--text-lg--line-height: calc(1.75 / 1.125);
|
||||
--text-xl: 1.25rem;
|
||||
@ -205,9 +206,6 @@
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.pointer-events-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
@ -217,9 +215,6 @@
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.top-1 {
|
||||
top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.top-1\/2 {
|
||||
top: calc(1/2 * 100%);
|
||||
}
|
||||
@ -229,9 +224,6 @@
|
||||
.left-0 {
|
||||
left: calc(var(--spacing) * 0);
|
||||
}
|
||||
.left-1 {
|
||||
left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.left-1\/2 {
|
||||
left: calc(1/2 * 100%);
|
||||
}
|
||||
@ -250,9 +242,6 @@
|
||||
.mx-4 {
|
||||
margin-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.mx-8 {
|
||||
margin-inline: calc(var(--spacing) * 8);
|
||||
}
|
||||
.my-2 {
|
||||
margin-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
@ -262,9 +251,6 @@
|
||||
.my-8 {
|
||||
margin-block: calc(var(--spacing) * 8);
|
||||
}
|
||||
.my-auto {
|
||||
margin-block: auto;
|
||||
}
|
||||
.mt-2 {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
@ -280,15 +266,18 @@
|
||||
.mt-16 {
|
||||
margin-top: calc(var(--spacing) * 16);
|
||||
}
|
||||
.mt-auto {
|
||||
margin-top: auto;
|
||||
}
|
||||
.mb-1 {
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
}
|
||||
.mb-2 {
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
.mb-6 {
|
||||
margin-bottom: calc(var(--spacing) * 6);
|
||||
}
|
||||
.mb-8 {
|
||||
margin-bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.mb-10 {
|
||||
margin-bottom: calc(var(--spacing) * 10);
|
||||
}
|
||||
@ -352,9 +341,6 @@
|
||||
.w-1\/3 {
|
||||
width: calc(1/3 * 100%);
|
||||
}
|
||||
.w-3 {
|
||||
width: calc(var(--spacing) * 3);
|
||||
}
|
||||
.w-3\/4 {
|
||||
width: calc(3/4 * 100%);
|
||||
}
|
||||
@ -367,21 +353,12 @@
|
||||
.w-5 {
|
||||
width: calc(var(--spacing) * 5);
|
||||
}
|
||||
.w-9 {
|
||||
width: calc(var(--spacing) * 9);
|
||||
}
|
||||
.w-9\/10 {
|
||||
width: calc(9/10 * 100%);
|
||||
}
|
||||
.w-24 {
|
||||
width: calc(var(--spacing) * 24);
|
||||
}
|
||||
.w-28 {
|
||||
width: calc(var(--spacing) * 28);
|
||||
}
|
||||
.w-32 {
|
||||
width: calc(var(--spacing) * 32);
|
||||
}
|
||||
.w-44 {
|
||||
width: calc(var(--spacing) * 44);
|
||||
}
|
||||
@ -397,27 +374,16 @@
|
||||
.max-w-xl {
|
||||
max-width: var(--container-xl);
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-1 {
|
||||
--tw-translate-x: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@ -425,8 +391,8 @@
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.resize {
|
||||
resize: both;
|
||||
.resize-none {
|
||||
resize: none;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
@ -458,6 +424,9 @@
|
||||
.gap-8 {
|
||||
gap: calc(var(--spacing) * 8);
|
||||
}
|
||||
.gap-x-1 {
|
||||
column-gap: calc(var(--spacing) * 1);
|
||||
}
|
||||
.gap-x-2 {
|
||||
column-gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
@ -467,9 +436,6 @@
|
||||
.gap-x-8 {
|
||||
column-gap: calc(var(--spacing) * 8);
|
||||
}
|
||||
.gap-x-16 {
|
||||
column-gap: calc(var(--spacing) * 16);
|
||||
}
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -492,10 +458,6 @@
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-1 {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-2 {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 2px;
|
||||
@ -529,15 +491,18 @@
|
||||
.border-gray-300 {
|
||||
border-color: var(--color-gray-300);
|
||||
}
|
||||
.border-red-400 {
|
||||
border-color: var(--color-red-400);
|
||||
}
|
||||
.border-red-500 {
|
||||
border-color: var(--color-red-500);
|
||||
}
|
||||
.border-white {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
.bg-blue-100 {
|
||||
background-color: var(--color-blue-100);
|
||||
}
|
||||
.bg-blue-500 {
|
||||
background-color: var(--color-blue-500);
|
||||
}
|
||||
.bg-gray-100 {
|
||||
background-color: var(--color-gray-100);
|
||||
}
|
||||
@ -559,6 +524,10 @@
|
||||
--tw-gradient-from: var(--color-blue-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.from-blue-200 {
|
||||
--tw-gradient-from: var(--color-blue-200);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.from-blue-400 {
|
||||
--tw-gradient-from: var(--color-blue-400);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
@ -571,15 +540,16 @@
|
||||
--tw-gradient-to: var(--color-purple-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-purple-200 {
|
||||
--tw-gradient-to: var(--color-purple-200);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.p-2 {
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
.p-4 {
|
||||
padding: calc(var(--spacing) * 4);
|
||||
}
|
||||
.p-8 {
|
||||
padding: calc(var(--spacing) * 8);
|
||||
}
|
||||
.px-1 {
|
||||
padding-inline: calc(var(--spacing) * 1);
|
||||
}
|
||||
@ -589,6 +559,9 @@
|
||||
.px-4 {
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.px-5 {
|
||||
padding-inline: calc(var(--spacing) * 5);
|
||||
}
|
||||
.px-8 {
|
||||
padding-inline: calc(var(--spacing) * 8);
|
||||
}
|
||||
@ -633,6 +606,10 @@
|
||||
font-size: var(--text-3xl);
|
||||
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
||||
}
|
||||
.text-base {
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--tw-leading, var(--text-base--line-height));
|
||||
}
|
||||
.text-lg {
|
||||
font-size: var(--text-lg);
|
||||
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||
@ -694,9 +671,6 @@
|
||||
.text-gray-800 {
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
.text-red-400 {
|
||||
color: var(--color-red-400);
|
||||
}
|
||||
.text-red-500 {
|
||||
color: var(--color-red-500);
|
||||
}
|
||||
@ -706,9 +680,6 @@
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
@ -787,6 +758,48 @@
|
||||
.\[-webkit-line-clamp\:4\] {
|
||||
-webkit-line-clamp: 4;
|
||||
}
|
||||
.file\:mr-4 {
|
||||
&::file-selector-button {
|
||||
margin-right: calc(var(--spacing) * 4);
|
||||
}
|
||||
}
|
||||
.file\:rounded-lg {
|
||||
&::file-selector-button {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
}
|
||||
.file\:border-0 {
|
||||
&::file-selector-button {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 0px;
|
||||
}
|
||||
}
|
||||
.file\:bg-blue-100 {
|
||||
&::file-selector-button {
|
||||
background-color: var(--color-blue-100);
|
||||
}
|
||||
}
|
||||
.file\:px-4 {
|
||||
&::file-selector-button {
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
}
|
||||
.file\:py-2 {
|
||||
&::file-selector-button {
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
}
|
||||
.file\:text-sm {
|
||||
&::file-selector-button {
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
}
|
||||
}
|
||||
.file\:text-blue-700 {
|
||||
&::file-selector-button {
|
||||
color: var(--color-blue-700);
|
||||
}
|
||||
}
|
||||
.hover\:cursor-pointer {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@ -801,6 +814,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-blue-200 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-blue-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-gray-50 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@ -815,13 +835,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-red-200 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-red-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:text-blue-400 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@ -930,6 +943,11 @@
|
||||
margin-inline: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
.md\:mx-16 {
|
||||
@media (width >= 48rem) {
|
||||
margin-inline: calc(var(--spacing) * 16);
|
||||
}
|
||||
}
|
||||
.md\:flex {
|
||||
@media (width >= 48rem) {
|
||||
display: flex;
|
||||
@ -1026,12 +1044,6 @@
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
}
|
||||
}
|
||||
.md\:text-xl {
|
||||
@media (width >= 48rem) {
|
||||
font-size: var(--text-xl);
|
||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||
}
|
||||
}
|
||||
.lg\:flex {
|
||||
@media (width >= 64rem) {
|
||||
display: flex;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user