nextpy-framework 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nextpy/__init__.py +50 -0
- nextpy/auth.py +94 -0
- nextpy/builder.py +123 -0
- nextpy/cli.py +490 -0
- nextpy/components/__init__.py +45 -0
- nextpy/components/feedback.py +210 -0
- nextpy/components/form.py +346 -0
- nextpy/components/head.py +167 -0
- nextpy/components/hooks_provider.py +64 -0
- nextpy/components/image.py +180 -0
- nextpy/components/layout.py +206 -0
- nextpy/components/link.py +132 -0
- nextpy/components/loader.py +65 -0
- nextpy/components/toast.py +101 -0
- nextpy/components/visual.py +185 -0
- nextpy/config.py +75 -0
- nextpy/core/__init__.py +21 -0
- nextpy/core/builder.py +237 -0
- nextpy/core/data_fetching.py +221 -0
- nextpy/core/renderer.py +252 -0
- nextpy/core/router.py +233 -0
- nextpy/core/sync.py +34 -0
- nextpy/db.py +121 -0
- nextpy/dev_server.py +69 -0
- nextpy/dev_tools.py +157 -0
- nextpy/errors.py +70 -0
- nextpy/hooks.py +348 -0
- nextpy/performance.py +78 -0
- nextpy/plugins.py +61 -0
- nextpy/py.typed +0 -0
- nextpy/server/__init__.py +6 -0
- nextpy/server/app.py +325 -0
- nextpy/server/debug.py +93 -0
- nextpy/server/middleware.py +88 -0
- nextpy/utils/__init__.py +0 -0
- nextpy/utils/cache.py +89 -0
- nextpy/utils/email.py +59 -0
- nextpy/utils/file_upload.py +65 -0
- nextpy/utils/logging.py +52 -0
- nextpy/utils/search.py +59 -0
- nextpy/utils/seo.py +85 -0
- nextpy/utils/validators.py +58 -0
- nextpy/websocket.py +76 -0
- nextpy_framework-1.0.0.dist-info/METADATA +343 -0
- nextpy_framework-1.0.0.dist-info/RECORD +49 -0
- nextpy_framework-1.0.0.dist-info/WHEEL +5 -0
- nextpy_framework-1.0.0.dist-info/entry_points.txt +2 -0
- nextpy_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
- nextpy_framework-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy Link Component - Client-side navigation with prefetching
|
|
3
|
+
Similar to Next.js's next/link with HTMX integration
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
from markupsafe import Markup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Link:
|
|
11
|
+
"""
|
|
12
|
+
Link component for client-side navigation
|
|
13
|
+
Uses HTMX for SPA-like navigation without full page reloads
|
|
14
|
+
|
|
15
|
+
Usage in templates:
|
|
16
|
+
{{ Link("/about", "About Us", prefetch=True) }}
|
|
17
|
+
{{ Link("/blog/" + post.slug, post.title, class_="text-blue-500") }}
|
|
18
|
+
|
|
19
|
+
In Python:
|
|
20
|
+
link = Link("/about", "About")
|
|
21
|
+
print(link.render())
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
href: str,
|
|
27
|
+
text: Optional[str] = None,
|
|
28
|
+
prefetch: bool = True,
|
|
29
|
+
replace: bool = False,
|
|
30
|
+
scroll: bool = True,
|
|
31
|
+
class_: Optional[str] = None,
|
|
32
|
+
target: Optional[str] = None,
|
|
33
|
+
use_htmx: bool = True,
|
|
34
|
+
**kwargs: Any,
|
|
35
|
+
):
|
|
36
|
+
self.href = href
|
|
37
|
+
self.text = text
|
|
38
|
+
self.prefetch = prefetch
|
|
39
|
+
self.replace = replace
|
|
40
|
+
self.scroll = scroll
|
|
41
|
+
self.class_ = class_
|
|
42
|
+
self.target = target
|
|
43
|
+
self.use_htmx = use_htmx
|
|
44
|
+
self.extra_attrs = kwargs
|
|
45
|
+
|
|
46
|
+
def render(self, children: Optional[str] = None) -> str:
|
|
47
|
+
"""Render the link as an HTML anchor tag"""
|
|
48
|
+
content = children or self.text or self.href
|
|
49
|
+
|
|
50
|
+
attrs = [f'href="{self._escape(self.href)}"']
|
|
51
|
+
|
|
52
|
+
if self.class_:
|
|
53
|
+
attrs.append(f'class="{self._escape(self.class_)}"')
|
|
54
|
+
|
|
55
|
+
if self.target:
|
|
56
|
+
attrs.append(f'target="{self._escape(self.target)}"')
|
|
57
|
+
|
|
58
|
+
if self.use_htmx and not self.target:
|
|
59
|
+
attrs.append(f'hx-get="{self._escape(self.href)}"')
|
|
60
|
+
attrs.append('hx-target="#main-content"')
|
|
61
|
+
attrs.append('hx-swap="innerHTML"')
|
|
62
|
+
attrs.append('hx-push-url="true"')
|
|
63
|
+
|
|
64
|
+
if self.prefetch:
|
|
65
|
+
attrs.append('hx-trigger="mouseenter, click"')
|
|
66
|
+
attrs.append('preload="true"')
|
|
67
|
+
|
|
68
|
+
elif self.prefetch:
|
|
69
|
+
attrs.append('data-prefetch="true"')
|
|
70
|
+
|
|
71
|
+
if self.replace:
|
|
72
|
+
attrs.append('data-replace="true"')
|
|
73
|
+
|
|
74
|
+
if not self.scroll:
|
|
75
|
+
attrs.append('data-scroll="false"')
|
|
76
|
+
|
|
77
|
+
for key, value in self.extra_attrs.items():
|
|
78
|
+
attr_name = key.replace("_", "-")
|
|
79
|
+
if value is True:
|
|
80
|
+
attrs.append(attr_name)
|
|
81
|
+
elif value is not False and value is not None:
|
|
82
|
+
attrs.append(f'{attr_name}="{self._escape(str(value))}"')
|
|
83
|
+
|
|
84
|
+
attrs_str = " ".join(attrs)
|
|
85
|
+
return f"<a {attrs_str}>{content}</a>"
|
|
86
|
+
|
|
87
|
+
def __html__(self) -> str:
|
|
88
|
+
"""Make the component work with Jinja2's autoescape"""
|
|
89
|
+
return self.render()
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str:
|
|
92
|
+
return self.render()
|
|
93
|
+
|
|
94
|
+
def __call__(
|
|
95
|
+
self,
|
|
96
|
+
href: Optional[str] = None,
|
|
97
|
+
text: Optional[str] = None,
|
|
98
|
+
**kwargs: Any
|
|
99
|
+
) -> Markup:
|
|
100
|
+
"""Allow calling Link() in templates with arguments"""
|
|
101
|
+
if href:
|
|
102
|
+
self.href = href
|
|
103
|
+
if text:
|
|
104
|
+
self.text = text
|
|
105
|
+
for key, value in kwargs.items():
|
|
106
|
+
if hasattr(self, key):
|
|
107
|
+
setattr(self, key, value)
|
|
108
|
+
else:
|
|
109
|
+
self.extra_attrs[key] = value
|
|
110
|
+
return Markup(self.render())
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _escape(value: str) -> str:
|
|
114
|
+
"""Escape HTML special characters"""
|
|
115
|
+
return (
|
|
116
|
+
str(value)
|
|
117
|
+
.replace("&", "&")
|
|
118
|
+
.replace("<", "<")
|
|
119
|
+
.replace(">", ">")
|
|
120
|
+
.replace('"', """)
|
|
121
|
+
.replace("'", "'")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def link(href: str, text: Optional[str] = None, **kwargs: Any) -> Markup:
|
|
126
|
+
"""
|
|
127
|
+
Functional helper to create a Link
|
|
128
|
+
|
|
129
|
+
Usage in templates:
|
|
130
|
+
{{ link("/about", "About Us") }}
|
|
131
|
+
"""
|
|
132
|
+
return Markup(Link(href, text, **kwargs).render())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loader/Spinner Components
|
|
3
|
+
Various loading indicators
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def spinner(size: str = "md", color: str = "blue") -> str:
|
|
8
|
+
"""Generate spinner HTML"""
|
|
9
|
+
sizes = {"sm": "w-4 h-4", "md": "w-8 h-8", "lg": "w-12 h-12"}
|
|
10
|
+
colors = {
|
|
11
|
+
"blue": "border-blue-500",
|
|
12
|
+
"red": "border-red-500",
|
|
13
|
+
"green": "border-green-500",
|
|
14
|
+
"purple": "border-purple-500"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return f'''
|
|
18
|
+
<div class="flex justify-center">
|
|
19
|
+
<div class="{sizes.get(size, sizes['md'])} border-4 border-gray-200 {colors.get(color, colors['blue'])} border-t-transparent rounded-full animate-spin"></div>
|
|
20
|
+
</div>
|
|
21
|
+
'''
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def skeleton(lines: int = 3, width: str = "full") -> str:
|
|
25
|
+
"""Generate skeleton loader (content placeholder)"""
|
|
26
|
+
skeleton_lines = "\n".join([
|
|
27
|
+
f'<div class="h-4 bg-gray-200 rounded mb-2 animate-pulse"></div>'
|
|
28
|
+
for _ in range(lines)
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
return f'''
|
|
32
|
+
<div class="w-{width}">
|
|
33
|
+
{skeleton_lines}
|
|
34
|
+
</div>
|
|
35
|
+
'''
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def progress_bar(value: int = 50, max_value: int = 100, color: str = "blue") -> str:
|
|
39
|
+
"""Generate progress bar"""
|
|
40
|
+
percentage = (value / max_value) * 100
|
|
41
|
+
colors = {
|
|
42
|
+
"blue": "bg-blue-500",
|
|
43
|
+
"red": "bg-red-500",
|
|
44
|
+
"green": "bg-green-500",
|
|
45
|
+
"purple": "bg-purple-500"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return f'''
|
|
49
|
+
<div class="w-full bg-gray-200 rounded-full h-2">
|
|
50
|
+
<div class="{colors.get(color, colors['blue'])} h-2 rounded-full" style="width: {percentage}%"></div>
|
|
51
|
+
</div>
|
|
52
|
+
<p class="text-sm text-gray-600 mt-1">{value}/{max_value}</p>
|
|
53
|
+
'''
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def loading_screen(message: str = "Loading...") -> str:
|
|
57
|
+
"""Full screen loading overlay"""
|
|
58
|
+
return f'''
|
|
59
|
+
<div class="fixed inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50">
|
|
60
|
+
<div class="text-center">
|
|
61
|
+
<div class="w-12 h-12 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin mx-auto mb-4"></div>
|
|
62
|
+
<p class="text-lg text-gray-700">{message}</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
'''
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Toast Notification Component
|
|
3
|
+
Display temporary alerts/notifications
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Toast:
|
|
10
|
+
"""Toast notification system"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.toasts = []
|
|
14
|
+
|
|
15
|
+
def add(self, message: str, type: Literal["success", "error", "info", "warning"] = "info", duration: int = 3000):
|
|
16
|
+
"""Add a toast notification"""
|
|
17
|
+
toast_id = len(self.toasts)
|
|
18
|
+
self.toasts.append({
|
|
19
|
+
"id": toast_id,
|
|
20
|
+
"message": message,
|
|
21
|
+
"type": type,
|
|
22
|
+
"duration": duration
|
|
23
|
+
})
|
|
24
|
+
return toast_id
|
|
25
|
+
|
|
26
|
+
def remove(self, toast_id: int):
|
|
27
|
+
"""Remove a toast"""
|
|
28
|
+
self.toasts = [t for t in self.toasts if t["id"] != toast_id]
|
|
29
|
+
|
|
30
|
+
def clear_all(self):
|
|
31
|
+
"""Clear all toasts"""
|
|
32
|
+
self.toasts = []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def toast_html() -> str:
|
|
36
|
+
"""Generate toast container HTML"""
|
|
37
|
+
return '''
|
|
38
|
+
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2" hx-ext="sse" sse-connect="/api/toasts">
|
|
39
|
+
<div hx-target="#toast-container" hx-swap="beforeend"></div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.toast {
|
|
44
|
+
animation: slideInRight 0.3s ease-out, slideOutRight 0.3s ease-in 3s forwards;
|
|
45
|
+
padding: 16px;
|
|
46
|
+
border-radius: 8px;
|
|
47
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.toast.success {
|
|
52
|
+
background-color: #10b981;
|
|
53
|
+
color: white;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.toast.error {
|
|
57
|
+
background-color: #ef4444;
|
|
58
|
+
color: white;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.toast.info {
|
|
62
|
+
background-color: #3b82f6;
|
|
63
|
+
color: white;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.toast.warning {
|
|
67
|
+
background-color: #f59e0b;
|
|
68
|
+
color: white;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@keyframes slideInRight {
|
|
72
|
+
from {
|
|
73
|
+
transform: translateX(400px);
|
|
74
|
+
opacity: 0;
|
|
75
|
+
}
|
|
76
|
+
to {
|
|
77
|
+
transform: translateX(0);
|
|
78
|
+
opacity: 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@keyframes slideOutRight {
|
|
83
|
+
from {
|
|
84
|
+
transform: translateX(0);
|
|
85
|
+
opacity: 1;
|
|
86
|
+
}
|
|
87
|
+
to {
|
|
88
|
+
transform: translateX(400px);
|
|
89
|
+
opacity: 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
93
|
+
'''
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Global toast instance
|
|
97
|
+
_toast = Toast()
|
|
98
|
+
|
|
99
|
+
def get_toast() -> Toast:
|
|
100
|
+
"""Get global toast instance"""
|
|
101
|
+
return _toast
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visual Components for NextPy
|
|
3
|
+
Tabs, Accordion, Dropdown, Modal, Card variations
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def Tabs(tabs: list, active_index: int = 0) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Tabs component
|
|
10
|
+
tabs: [{"label": "Tab 1", "content": "<p>Content 1</p>"}, ...]
|
|
11
|
+
"""
|
|
12
|
+
tab_buttons_list = []
|
|
13
|
+
for i, tab in enumerate(tabs):
|
|
14
|
+
active_class = "border-blue-600 text-blue-600 font-bold" if i == active_index else "border-gray-300 text-gray-600"
|
|
15
|
+
tab_buttons_list.append(f'<button class="px-4 py-2 border-b-2 {active_class}" onclick="switchTab({i})">\n {tab["label"]}\n </button>')
|
|
16
|
+
tab_buttons = "\n".join(tab_buttons_list)
|
|
17
|
+
|
|
18
|
+
tab_contents_list = []
|
|
19
|
+
for i, tab in enumerate(tabs):
|
|
20
|
+
display_class = "block" if i == active_index else "hidden"
|
|
21
|
+
tab_contents_list.append(f'<div id="tab-content-{i}" class="{display_class} py-4">\n {tab["content"]}\n </div>')
|
|
22
|
+
tab_contents = "\n".join(tab_contents_list)
|
|
23
|
+
|
|
24
|
+
return f'''
|
|
25
|
+
<div class="space-y-4">
|
|
26
|
+
<div class="flex border-b border-gray-200">
|
|
27
|
+
{tab_buttons}
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
{tab_contents}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<script>
|
|
34
|
+
function switchTab(index) {{
|
|
35
|
+
document.querySelectorAll('[id^="tab-content-"]').forEach(el => el.classList.add('hidden'));
|
|
36
|
+
document.getElementById('tab-content-' + index).classList.remove('hidden');
|
|
37
|
+
document.querySelectorAll('button').forEach((btn, i) => {{
|
|
38
|
+
btn.classList.toggle('border-blue-600', i === index);
|
|
39
|
+
btn.classList.toggle('text-blue-600', i === index);
|
|
40
|
+
btn.classList.toggle('font-bold', i === index);
|
|
41
|
+
btn.classList.toggle('border-gray-300', i !== index);
|
|
42
|
+
btn.classList.toggle('text-gray-600', i !== index);
|
|
43
|
+
}});
|
|
44
|
+
}}
|
|
45
|
+
</script>
|
|
46
|
+
'''
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def Accordion(items: list) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Accordion component
|
|
52
|
+
items: [{"title": "Section 1", "content": "<p>Content 1</p>"}, ...]
|
|
53
|
+
"""
|
|
54
|
+
accordion_items = []
|
|
55
|
+
for i, item in enumerate(items):
|
|
56
|
+
accordion_items.append(f'''
|
|
57
|
+
<div class="border border-gray-300 mb-2 rounded-lg overflow-hidden">
|
|
58
|
+
<button onclick="toggleAccordion({i})" class="w-full text-left px-4 py-3 bg-gray-100 hover:bg-gray-200 font-semibold flex justify-between items-center">
|
|
59
|
+
{item["title"]}
|
|
60
|
+
<span id="accordion-icon-{i}">▼</span>
|
|
61
|
+
</button>
|
|
62
|
+
<div id="accordion-content-{i}" class="hidden px-4 py-3 bg-white text-gray-700">
|
|
63
|
+
{item["content"]}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
''')
|
|
67
|
+
accordion_html = "\n".join(accordion_items)
|
|
68
|
+
|
|
69
|
+
return f'''
|
|
70
|
+
<div class="space-y-2">
|
|
71
|
+
{accordion_html}
|
|
72
|
+
</div>
|
|
73
|
+
<script>
|
|
74
|
+
function toggleAccordion(index) {{
|
|
75
|
+
const content = document.getElementById('accordion-content-' + index);
|
|
76
|
+
const icon = document.getElementById('accordion-icon-' + index);
|
|
77
|
+
content.classList.toggle('hidden');
|
|
78
|
+
icon.textContent = content.classList.contains('hidden') ? '▼' : '▲';
|
|
79
|
+
}}
|
|
80
|
+
</script>
|
|
81
|
+
'''
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def Dropdown(label: str, items: list, position: str = "left") -> str:
|
|
85
|
+
"""
|
|
86
|
+
Dropdown component
|
|
87
|
+
items: [{"label": "Option 1", "href": "/path"}, ...]
|
|
88
|
+
"""
|
|
89
|
+
position_class = "right-0" if position == "right" else "left-0"
|
|
90
|
+
items_html = "\n".join([
|
|
91
|
+
f'<a href="{item.get("href", "#")}" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">{item["label"]}</a>'
|
|
92
|
+
for item in items
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
return f'''
|
|
96
|
+
<div class="relative inline-block">
|
|
97
|
+
<button onclick="toggleDropdown()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
|
98
|
+
{label}
|
|
99
|
+
</button>
|
|
100
|
+
<div id="dropdown-menu" class="hidden absolute {position_class} mt-2 bg-white border border-gray-300 rounded-lg shadow-lg z-10">
|
|
101
|
+
{items_html}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<script>
|
|
105
|
+
function toggleDropdown() {{
|
|
106
|
+
const menu = document.getElementById('dropdown-menu');
|
|
107
|
+
menu.classList.toggle('hidden');
|
|
108
|
+
}}
|
|
109
|
+
document.addEventListener('click', function(event) {{
|
|
110
|
+
const menu = document.getElementById('dropdown-menu');
|
|
111
|
+
if (!menu.parentElement.contains(event.target)) menu.classList.add('hidden');
|
|
112
|
+
}});
|
|
113
|
+
</script>
|
|
114
|
+
'''
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def Modal(title: str, content: str, footer: str = "", show: bool = False) -> str:
|
|
118
|
+
"""Modal component"""
|
|
119
|
+
display = "flex" if show else "hidden"
|
|
120
|
+
footer_html = f'<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-2">{footer}</div>' if footer else ''
|
|
121
|
+
return f'''
|
|
122
|
+
<div id="modal-overlay" class="{display} fixed inset-0 bg-black bg-opacity-50 justify-center items-center z-50">
|
|
123
|
+
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
|
124
|
+
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
|
125
|
+
<h2 class="text-xl font-bold">{title}</h2>
|
|
126
|
+
<button onclick="closeModal()" class="text-gray-500 hover:text-gray-700 text-2xl">×</button>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="px-6 py-4">
|
|
129
|
+
{content}
|
|
130
|
+
</div>
|
|
131
|
+
{footer_html}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<script>
|
|
135
|
+
function openModal() {{
|
|
136
|
+
document.getElementById('modal-overlay').classList.remove('hidden');
|
|
137
|
+
document.getElementById('modal-overlay').classList.add('flex');
|
|
138
|
+
}}
|
|
139
|
+
function closeModal() {{
|
|
140
|
+
document.getElementById('modal-overlay').classList.add('hidden');
|
|
141
|
+
document.getElementById('modal-overlay').classList.remove('flex');
|
|
142
|
+
}}
|
|
143
|
+
</script>
|
|
144
|
+
'''
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def Card(title: str, content: str, image: str = "", footer: str = "") -> str:
|
|
148
|
+
"""Enhanced Card component"""
|
|
149
|
+
image_html = f'<img src="{image}" class="w-full h-48 object-cover rounded-t-lg"/>' if image else ""
|
|
150
|
+
footer_html = f'<div class="px-6 py-3 bg-gray-50 border-t border-gray-200 text-sm text-gray-600">{footer}</div>' if footer else ""
|
|
151
|
+
|
|
152
|
+
return f'''
|
|
153
|
+
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition">
|
|
154
|
+
{image_html}
|
|
155
|
+
<div class="px-6 py-4">
|
|
156
|
+
<h3 class="font-bold text-lg mb-2">{title}</h3>
|
|
157
|
+
<p class="text-gray-600">{content}</p>
|
|
158
|
+
</div>
|
|
159
|
+
{footer_html}
|
|
160
|
+
</div>
|
|
161
|
+
'''
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def Breadcrumb(items: list) -> str:
|
|
165
|
+
"""Breadcrumb navigation"""
|
|
166
|
+
breadcrumb_parts = []
|
|
167
|
+
for item in items:
|
|
168
|
+
if item.get("href"):
|
|
169
|
+
breadcrumb_parts.append(f'<a href="{item.get("href")}" class="text-blue-600 hover:underline">{item["label"]}</a>')
|
|
170
|
+
else:
|
|
171
|
+
breadcrumb_parts.append(f'<span class="text-gray-700">{item["label"]}</span>')
|
|
172
|
+
breadcrumb_html = " / ".join(breadcrumb_parts)
|
|
173
|
+
return f'<nav class="text-sm text-gray-600 mb-4">{breadcrumb_html}</nav>'
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def Pagination(current: int = 1, total: int = 10, base_url: str = "") -> str:
|
|
177
|
+
"""Pagination component"""
|
|
178
|
+
pages = []
|
|
179
|
+
for i in range(1, total + 1):
|
|
180
|
+
if i == current:
|
|
181
|
+
pages.append(f'<span class="px-3 py-1 bg-blue-600 text-white rounded">{i}</span>')
|
|
182
|
+
else:
|
|
183
|
+
pages.append(f'<a href="{base_url}?page={i}" class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100">{i}</a>')
|
|
184
|
+
|
|
185
|
+
return f'<div class="flex gap-2">{" ".join(pages)}</div>'
|
nextpy/config.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy Configuration Manager
|
|
3
|
+
Environment variables, settings, and configuration
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional, Any
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
10
|
+
|
|
11
|
+
# Load .env file
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Settings(BaseSettings):
|
|
16
|
+
"""Application settings from environment variables"""
|
|
17
|
+
|
|
18
|
+
# App
|
|
19
|
+
app_name: str = "NextPy App"
|
|
20
|
+
debug: bool = os.getenv("DEBUG", "true").lower() == "true"
|
|
21
|
+
secret_key: str = os.getenv("SECRET_KEY", "change-me-in-production")
|
|
22
|
+
domain: str = os.getenv("DOMAIN", "localhost:5000")
|
|
23
|
+
|
|
24
|
+
# Database
|
|
25
|
+
database_url: str = os.getenv("DATABASE_URL", "sqlite:///./nextpy.db")
|
|
26
|
+
db_echo: bool = os.getenv("DB_ECHO", "false").lower() == "true"
|
|
27
|
+
|
|
28
|
+
# Auth
|
|
29
|
+
jwt_secret: str = os.getenv("JWT_SECRET", "change-me")
|
|
30
|
+
jwt_algorithm: str = "HS256"
|
|
31
|
+
jwt_expiration_hours: int = 24
|
|
32
|
+
session_secret: str = os.getenv("SESSION_SECRET", "change-me")
|
|
33
|
+
|
|
34
|
+
# Email
|
|
35
|
+
mail_server: str = os.getenv("MAIL_SERVER", "smtp.gmail.com")
|
|
36
|
+
mail_port: int = int(os.getenv("MAIL_PORT", "587"))
|
|
37
|
+
mail_username: str = os.getenv("MAIL_USERNAME", "")
|
|
38
|
+
mail_password: str = os.getenv("MAIL_PASSWORD", "")
|
|
39
|
+
|
|
40
|
+
# API Keys
|
|
41
|
+
api_key: Optional[str] = os.getenv("API_KEY")
|
|
42
|
+
stripe_key: Optional[str] = os.getenv("STRIPE_KEY")
|
|
43
|
+
openai_api_key: Optional[str] = os.getenv("OPENAI_API_KEY")
|
|
44
|
+
|
|
45
|
+
# URLs
|
|
46
|
+
frontend_url: str = os.getenv("FRONTEND_URL", "http://localhost:5000")
|
|
47
|
+
backend_url: str = os.getenv("BACKEND_URL", "http://localhost:5000")
|
|
48
|
+
|
|
49
|
+
class Config:
|
|
50
|
+
env_file = ".env"
|
|
51
|
+
case_sensitive = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Global settings instance
|
|
55
|
+
settings = Settings()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_setting(key: str, default: Any = None) -> Any:
|
|
59
|
+
"""Get setting by key"""
|
|
60
|
+
return getattr(settings, key, default)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_env(key: str, default: Any = None) -> Any:
|
|
64
|
+
"""Get environment variable"""
|
|
65
|
+
return os.getenv(key, default)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_production() -> bool:
|
|
69
|
+
"""Check if running in production"""
|
|
70
|
+
return not settings.debug
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_development() -> bool:
|
|
74
|
+
"""Check if running in development"""
|
|
75
|
+
return settings.debug
|
nextpy/core/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""NextPy Core Module - Routing, Rendering, and Data Fetching"""
|
|
2
|
+
|
|
3
|
+
from nextpy.core.router import Router, Route, DynamicRoute
|
|
4
|
+
from nextpy.core.renderer import Renderer
|
|
5
|
+
from nextpy.core.data_fetching import (
|
|
6
|
+
get_server_side_props,
|
|
7
|
+
get_static_props,
|
|
8
|
+
get_static_paths,
|
|
9
|
+
)
|
|
10
|
+
from nextpy.core.builder import Builder
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Router",
|
|
14
|
+
"Route",
|
|
15
|
+
"DynamicRoute",
|
|
16
|
+
"Renderer",
|
|
17
|
+
"get_server_side_props",
|
|
18
|
+
"get_static_props",
|
|
19
|
+
"get_static_paths",
|
|
20
|
+
"Builder",
|
|
21
|
+
]
|