From 345a801c40c1c5acb40cd14dfb68a5377805b524 Mon Sep 17 00:00:00 2001 From: pika Date: Mon, 14 Apr 2025 22:25:26 +0200 Subject: [PATCH] batman --- README.md | 85 + app.py | 4 + app/__init__.py | 56 + app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 2496 bytes app/__pycache__/routes.cpython-313.pyc | Bin 0 -> 15001 bytes app/auth/__init__.py | 5 + app/auth/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 293 bytes app/auth/__pycache__/forms.cpython-313.pyc | Bin 0 -> 1941 bytes app/auth/__pycache__/routes.cpython-313.pyc | Bin 0 -> 4571 bytes app/auth/forms.py | 21 + app/auth/routes.py | 77 + app/models/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 163 bytes .../__pycache__/document.cpython-313.pyc | Bin 0 -> 6298 bytes app/models/__pycache__/user.cpython-313.pyc | Bin 0 -> 2680 bytes app/models/document.py | 84 + app/models/user.py | 34 + app/routes.py | 268 +++ app/static/css/style.css | 691 +++++++ app/static/fonts/CascadiaCove.ttf | 1817 +++++++++++++++++ app/static/js/app.js | 121 ++ app/templates/auth/login.html | 72 + app/templates/auth/settings.html | 89 + app/templates/auth/signup.html | 73 + app/templates/base.html | 332 +++ app/templates/category.html | 364 ++++ app/templates/document_edit.html | 491 +++++ app/templates/document_view.html | 278 +++ app/templates/index.html | 400 ++++ create_db.py | 117 ++ instance/docs.db | Bin 0 -> 36864 bytes requirements.txt | 13 + run.py | 6 + 33 files changed, 5499 insertions(+) create mode 100644 README.md create mode 100644 app.py create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-313.pyc create mode 100644 app/__pycache__/routes.cpython-313.pyc create mode 100644 app/auth/__init__.py create mode 100644 app/auth/__pycache__/__init__.cpython-313.pyc create mode 100644 app/auth/__pycache__/forms.cpython-313.pyc create mode 100644 app/auth/__pycache__/routes.cpython-313.pyc create mode 100644 app/auth/forms.py create mode 100644 app/auth/routes.py create mode 100644 app/models/__init__.py create mode 100644 app/models/__pycache__/__init__.cpython-313.pyc create mode 100644 app/models/__pycache__/document.cpython-313.pyc create mode 100644 app/models/__pycache__/user.cpython-313.pyc create mode 100644 app/models/document.py create mode 100644 app/models/user.py create mode 100644 app/routes.py create mode 100644 app/static/css/style.css create mode 100644 app/static/fonts/CascadiaCove.ttf create mode 100644 app/static/js/app.js create mode 100644 app/templates/auth/login.html create mode 100644 app/templates/auth/settings.html create mode 100644 app/templates/auth/signup.html create mode 100644 app/templates/base.html create mode 100644 app/templates/category.html create mode 100644 app/templates/document_edit.html create mode 100644 app/templates/document_view.html create mode 100644 app/templates/index.html create mode 100644 create_db.py create mode 100644 instance/docs.db create mode 100644 requirements.txt create mode 100644 run.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..0618107 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Vim Docs - Markdown Documentation Platform + +A Flask-based documentation platform with Vim editing capabilities, designed for developers and tech enthusiasts who prefer keyboard-centric navigation and editing. + +## Features + +- **Vim Editing**: Full Vim keybindings for editing your documents +- **Markdown Support**: GitHub-style markdown rendering with support for tables, code blocks, and alert blocks +- **Real-time Preview**: Split-screen editing with synchronized scrolling between editor and preview +- **Organization**: Hierarchical categories with customizable icons +- **Tags**: Tag-based document organization and filtering +- **Export**: Export your documents as markdown files +- **Keyboard Shortcuts**: Navigate the app efficiently with Vim-inspired keyboard shortcuts + +## Setup Instructions + +### Prerequisites + +- Python 3.7+ +- pip (Python package manager) + +### Installation + +1. Clone this repository: + ``` + git clone https://github.com/yourusername/vim-docs.git + cd vim-docs + ``` + +2. Create and activate a virtual environment (recommended): + ``` + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +3. Install dependencies: + ``` + pip install -r requirements.txt + ``` + +4. Initialize the database: + ``` + python app.py + ``` + +5. Access the application: + Open your browser and navigate to `http://localhost:5000` + +## Keyboard Shortcuts + +- `Ctrl+E`: Edit the current document (when viewing) +- `n`: Create a new document +- `/`: Focus the search box (if available) +- `g h`: Go to the home page + +## Within the Editor + +- Standard Vim keybindings (`h`, `j`, `k`, `l` for navigation, etc.) +- `Ctrl+S`: Save the current document + +## Alert Blocks + +You can create GitHub-style alert blocks in your markdown: + +``` +> [!INFO] +> This is an information alert. + +> [!WARNING] +> This is a warning alert. + +> [!DANGER] +> This is a danger alert. +``` + +## License + +MIT License + +## Credits + +- CodeMirror for the Vim editor implementation +- Marked.js for Markdown parsing +- Material Design Icons for beautiful iconography +- CascadyaCove Nerd Font for editor typography \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..eed53d0 --- /dev/null +++ b/app.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..0e99557 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,56 @@ +# App package initialization +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_migrate import Migrate +from flask_wtf.csrf import CSRFProtect +import os +from datetime import timedelta + +# Initialize SQLAlchemy outside of create_app +db = SQLAlchemy() +login_manager = LoginManager() +csrf = CSRFProtect() +migrate = Migrate() + +# App configuration +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(24) + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///docs.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False + PERMANENT_SESSION_LIFETIME = timedelta(hours=12) + SESSION_TYPE = 'filesystem' + +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(config_class) + + # Initialize extensions + db.init_app(app) + login_manager.init_app(app) + csrf.init_app(app) + migrate.init_app(app, db) + + # Configure login manager + login_manager.login_view = 'auth.login' + login_manager.login_message = 'Please log in to access this page.' + login_manager.login_message_category = 'info' + + # Configure session + app.config['SESSION_PERMANENT'] = True + + # Register blueprints + from app.routes import main as main_bp + app.register_blueprint(main_bp) + + from app.auth import bp as auth_bp + app.register_blueprint(auth_bp, url_prefix='/auth') + + return app + +# Create app instance +app = create_app() + +# Import models after db initialization to avoid circular imports +from app.models.document import Document, Category, Tag +from app.models.user import User \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..039e4643322488475da630537134f1a9310593e9 GIT binary patch literal 2496 zcmb7G&2Jk;6rZ)%Uu!##lQd~f>u!>Mxou)0TKbg&sq2W?aT>f1QdPsSvDeN9dpDh3 zwnr3k8038@^*1yyi>zkqY1MoKay2nqENPOVbZ6B2LMUdKp4#YlehzGmLr z_hx?M?O;$qFn)jU&$8l1=r<Tm)@?CA--4M~A?n&(;yHdTRH`Pb_tZV}5-KjlfkBG|d zSa6GN6XU3BtRfp90wYN8OpI0vvr2UVq`-t;R<(?*$z_Fr?8&HQBAW`Z{)Chrnc~_wn;D%{181CuWDv|Ru3(W zt5%0VZ^Y&K`8dX^rkWTJ&o2;mhAhHLZ%hIC6s?83??pa~+>3q|{W|bCJhU2q^HJ9i zk?$fuUidy*Ppw782Xpu59%%Qqhwnd*j;uzdrMILd{*|?ZN0<14_5P!ex|aJ-Eb-B` z?)^6>h-il>G>;+32)W?rgx~g0puLN(v}{+JmlGlTIBDE8m$k0~TiYC2kIn7O)2&q^ zy22`LjjZ41cIIi$-XAo9C5xdBE4si5W%5%}%1Bkau6EwF%W+>$pPN_A=g*8k$|lzftK#@g)w ziuorO1-WL<4p$iPEe=do6xmQjkVI7zOL2D4*ke$f?YyMrLqNCnRmrKElo3lnyXNH-AbE?781{UxGTu|6{g@Jb!U z8S1r8JRE)+v~t7M1}am&V2~2^R2#@lg@*N-s$6e*LdK9P?QJe>h`0biUe?J16=2t4 zqO)sB4zeV=h*DModKT zVG0d4G|i3@_;Z42M?-hrEDf`M69yclk6F9`nF@P+Em$T;nc+AiLyQbFVnHM;IC&ii z?PjfnM?|T>+tY+va+aCTF*3q@LTx@Z_!e@WnHXd@(9r6qcck$Yy+Ovn!0^xb0LW7( z$8k?k-zGY|i4JU{{!Mh~XXM*N(PwVt?!1|=JJvh*e%?_Ju6w#}eq86*h4AfI-Sfij n;>2eN$kPtAZ}2(V!-bxmMxOA^O5ORwjkwdx=+rMrv@-Y$Ikq{} literal 0 HcmV?d00001 diff --git a/app/__pycache__/routes.cpython-313.pyc b/app/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e35c0ac182e4ca38d936d09d8c25d9c82a3077dd GIT binary patch literal 15001 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 literal 0 HcmV?d00001 diff --git a/app/models/__pycache__/document.cpython-313.pyc b/app/models/__pycache__/document.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58f71a20afa01e623a1f15d454cfcb339f57cd26 GIT binary patch literal 6298 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 K;yW$P3jYs+Q8$|a literal 0 HcmV?d00001 diff --git a/app/models/document.py b/app/models/document.py new file mode 100644 index 0000000..7958751 --- /dev/null +++ b/app/models/document.py @@ -0,0 +1,84 @@ +from app import db +from datetime import datetime +from flask import url_for +import json + +# Association table for document-tag many-to-many relationship +document_tags = db.Table('document_tags', + db.Column('document_id', db.Integer, db.ForeignKey('document.id'), primary_key=True), + db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True) +) + +class Document(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + content = db.Column(db.Text, nullable=False, default='') + created_date = db.Column(db.DateTime, default=datetime.utcnow) + updated_date = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + category_id = db.Column(db.Integer, db.ForeignKey('category.id')) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + tags = db.relationship('Tag', secondary=document_tags, backref=db.backref('documents', lazy='dynamic')) + + def __repr__(self): + return f'' + + def to_dict(self): + return { + 'id': self.id, + 'title': self.title, + 'content': self.content, + 'created_date': self.created_date.isoformat(), + 'updated_date': self.updated_date.isoformat(), + 'category_id': self.category_id, + 'user_id': self.user_id, + 'tags': [tag.name for tag in self.tags] + } + +class Category(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + icon = db.Column(db.String(100), default='mdi-folder-outline') # Material Design Icons + description = db.Column(db.String(200)) + parent_id = db.Column(db.Integer, db.ForeignKey('category.id')) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + is_root = db.Column(db.Boolean, default=False) + documents = db.relationship('Document', backref='category', lazy='dynamic') + children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]), lazy='dynamic') + + def __repr__(self): + return f'' + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'icon': self.icon, + 'description': self.description, + 'parent_id': self.parent_id, + 'user_id': self.user_id, + 'is_root': self.is_root, + 'children': [child.to_dict() for child in self.children], + 'documents': [doc.id for doc in self.documents] + } + +class Tag(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + color = db.Column(db.String(20), default='#50fa7b') + + __table_args__ = ( + db.UniqueConstraint('name', 'user_id', name='_tag_user_uc'), + ) + + def __repr__(self): + return f'' + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'user_id': self.user_id, + 'color': self.color, + 'document_count': self.documents.count() + } \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..0a5e233 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,34 @@ +from app import db, login_manager +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), index=True, unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + created_date = db.Column(db.DateTime, default=datetime.utcnow) + theme_color = db.Column(db.String(20), default='#50fa7b') # Default green color + documents = db.relationship('Document', backref='author', lazy='dynamic') + categories = db.relationship('Category', backref='owner', lazy='dynamic') + + def __repr__(self): + return f'' + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def to_dict(self): + return { + 'id': self.id, + 'username': self.username, + 'created_date': self.created_date.isoformat(), + 'theme_color': self.theme_color + } + +@login_manager.user_loader +def load_user(id): + return User.query.get(int(id)) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..bbf63b6 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,268 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_file +from flask_login import current_user, login_required +from app import db +from app.models.document import Document, Category, Tag +from app.models.user import User +import json +from datetime import datetime +import io +from sqlalchemy import or_ + +main = Blueprint('main', __name__) + +@main.route('/') +@login_required +def index(): + """Home page showing categories and recent documents""" + root_categories = Category.query.filter_by( + user_id=current_user.id, + parent_id=None + ).all() + + recent_docs = Document.query.filter_by( + user_id=current_user.id + ).order_by(Document.updated_date.desc()).limit(5).all() + + return render_template('index.html', categories=root_categories, recent_docs=recent_docs) + +@main.route('/category/') +@login_required +def view_category(category_id): + """View a specific category and its documents""" + category = Category.query.filter_by(id=category_id, user_id=current_user.id).first_or_404() + return render_template('category.html', category=category) + +@main.route('/document/', methods=['GET']) +@login_required +def view_document(doc_id): + """View a document in read mode""" + document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404() + return render_template('document_view.html', document=document) + +@main.route('/document//edit', methods=['GET']) +@login_required +def edit_document(doc_id): + """Edit a document with the Vim editor""" + document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404() + categories = Category.query.filter_by(user_id=current_user.id).all() + tags = Tag.query.filter_by(user_id=current_user.id).all() + return render_template('document_edit.html', document=document, categories=categories, tags=tags) + +@main.route('/document/new', methods=['GET']) +@login_required +def new_document(): + """Create a new document with the Vim editor""" + categories = Category.query.filter_by(user_id=current_user.id).all() + tags = Tag.query.filter_by(user_id=current_user.id).all() + category_id = request.args.get('category') + + document = None + + if category_id: + category = Category.query.filter_by(id=category_id, user_id=current_user.id).first() + if category: + document = Document( + title="Untitled Document", + content="", + category_id=category_id, + user_id=current_user.id + ) + + return render_template('document_edit.html', document=document, categories=categories, tags=tags, preselected_category_id=category_id) + +@main.route('/api/document', methods=['POST']) +@login_required +def save_document(): + """Save a document (new or existing)""" + data = request.json + + # Get root category as default + root_category = Category.query.filter_by(user_id=current_user.id, is_root=True).first() + default_category_id = root_category.id if root_category else None + + if 'id' in data and data['id']: + # Update existing document - verify ownership + document = Document.query.filter_by(id=data['id'], user_id=current_user.id).first_or_404() + document.title = data['title'] + document.content = data['content'] + document.category_id = data['category_id'] if data['category_id'] else default_category_id + else: + # Create new document + document = Document( + title=data['title'], + content=data['content'], + category_id=data['category_id'] if data['category_id'] else default_category_id, + user_id=current_user.id + ) + db.session.add(document) + + # Handle tags + if 'tags' in data: + document.tags = [] + for tag_name in data['tags']: + tag = Tag.query.filter_by(name=tag_name, user_id=current_user.id).first() + if not tag: + tag = Tag(name=tag_name, user_id=current_user.id) + db.session.add(tag) + document.tags.append(tag) + + db.session.commit() + return jsonify(document.to_dict()) + +@main.route('/api/document/', methods=['DELETE']) +@login_required +def delete_document(doc_id): + """Delete a document""" + document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404() + db.session.delete(document) + db.session.commit() + return jsonify({'success': True}) + +@main.route('/api/category', methods=['POST']) +@login_required +def save_category(): + """Save a category (new or existing)""" + data = request.json + + if 'id' in data and data['id']: + # Update existing category - verify ownership + category = Category.query.filter_by(id=data['id'], user_id=current_user.id).first_or_404() + category.name = data['name'] + category.icon = data['icon'] + category.description = data.get('description', '') + + # Only change parent if it belongs to the same user + if data.get('parent_id'): + parent = Category.query.filter_by(id=data['parent_id'], user_id=current_user.id).first() + if parent: + category.parent_id = parent.id + else: + # Create new category + category = Category( + name=data['name'], + icon=data['icon'], + description=data.get('description', ''), + parent_id=data['parent_id'] if data.get('parent_id') else None, + user_id=current_user.id + ) + db.session.add(category) + + db.session.commit() + return jsonify(category.to_dict()) + +@main.route('/api/category/', methods=['DELETE']) +@login_required +def delete_category(category_id): + """Delete a category and optionally reassign documents""" + category = Category.query.filter_by(id=category_id, user_id=current_user.id).first_or_404() + + # Can't delete root category + if category.is_root: + return jsonify({'error': 'Cannot delete root category'}), 400 + + # Get target category for documents if specified + new_category_id = request.args.get('new_category_id') + if new_category_id: + new_category = Category.query.filter_by(id=new_category_id, user_id=current_user.id).first() + if new_category: + # Reassign documents + for doc in category.documents: + doc.category_id = new_category.id + else: + # Move documents to no category + for doc in category.documents: + doc.category_id = None + + # Also handle child categories + for child in category.children: + if new_category_id: + child.parent_id = new_category_id + else: + child.parent_id = None + + db.session.delete(category) + db.session.commit() + return jsonify({'success': True}) + +@main.route('/api/search', methods=['GET']) +@login_required +def search_documents(): + """Search for documents by title, content, or tags""" + query = request.args.get('q', '') + + if not query or len(query) < 2: + return jsonify({'results': []}) + + # Search in title, content, and tags for current user's documents only + docs = Document.query.filter( + Document.user_id == current_user.id, + or_( + Document.title.ilike(f'%{query}%'), + Document.content.ilike(f'%{query}%'), + Document.tags.any(Tag.name.ilike(f'%{query}%')) + ) + ).limit(10).all() + + results = [] + for doc in docs: + # Find match in content + match = None + if query.lower() in doc.content.lower(): + # Find the sentence containing the match + content_lower = doc.content.lower() + query_pos = content_lower.find(query.lower()) + + # Get a snippet around the match + start = max(0, content_lower.rfind('.', 0, query_pos) + 1) + end = content_lower.find('.', query_pos) + if end == -1: + end = min(len(doc.content), query_pos + 200) + + match = doc.content[start:end].strip() + + # Get category name + category_name = doc.category.name if doc.category else None + + results.append({ + 'id': doc.id, + 'title': doc.title, + 'category': category_name, + 'tags': [tag.name for tag in doc.tags], + 'match': match + }) + + return jsonify({'results': results}) + +@main.route('/api/tags', methods=['GET']) +@login_required +def get_tags(): + """Get all tags for the current user""" + tags = Tag.query.filter_by(user_id=current_user.id).all() + return jsonify({'tags': [tag.to_dict() for tag in tags]}) + +@main.route('/document//export', methods=['GET']) +@login_required +def export_document(doc_id): + """Export a document as a markdown file""" + document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404() + + # Create a file-like object in memory + file_data = io.BytesIO(document.content.encode('utf-8')) + file_data.seek(0) + + return send_file( + file_data, + mimetype='text/markdown', + as_attachment=True, + download_name=f"{document.title.replace(' ', '_')}.md" + ) + +@main.route('/api/categories', methods=['GET']) +@login_required +def get_categories(): + """Get all root categories with their children for the current user""" + root_categories = Category.query.filter_by( + user_id=current_user.id, + parent_id=None + ).all() + return jsonify([category.to_dict() for category in root_categories]) \ No newline at end of file diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000..f0613ba --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,691 @@ +/* Main CSS Variables */ +:root { + --bg-color: #1a1a1a; + --bg-light: #2a2a2a; + --text-color: #f8f8f2; + --text-muted: #a0a0a0; + --border-color: #3a3a3a; + --accent-color: #8be9fd; + --primary-color: #50fa7b; + --danger-color: #ff5555; + --warning-color: #ffb86c; + --info-color: #8be9fd; + --caution-color: #f1fa8c; + --note-color: #bd93f9; + --sidebar-width: 280px; + --header-height: 64px; + --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +/* Base styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-family); + background-color: var(--bg-color); + color: var(--text-color); + line-height: 1.6; + height: 100vh; + overflow: hidden; +} + +a { + color: var(--accent-color); + text-decoration: none; +} + +button { + cursor: pointer; + background: none; + border: none; + color: inherit; +} + +/* App layout */ +.app-container { + display: flex; + height: 100vh; + overflow: hidden; +} + +/* Sidebar */ +.sidebar { + width: var(--sidebar-width); + background-color: var(--bg-light); + border-right: 1px solid var(--border-color); + height: 100vh; + overflow-y: auto; + transition: transform 0.3s ease; +} + +.sidebar-header { + padding: 20px; + border-bottom: 1px solid var(--border-color); +} + +.sidebar-header h1 { + font-size: 1.5rem; + display: flex; + align-items: center; +} + +.sidebar-header h1 i { + margin-right: 10px; + color: var(--primary-color); +} + +.sidebar-nav { + padding: 15px 0; +} + +.sidebar-nav ul { + list-style: none; +} + +.sidebar-nav li { + padding: 8px 20px; +} + +.sidebar-nav li a { + display: flex; + align-items: center; + color: var(--text-color); + padding: 8px 12px; + border-radius: 6px; + transition: all 0.2s ease; +} + +.sidebar-nav li a:hover { + color: var(--primary-color); + background-color: rgba(80, 250, 123, 0.1); + transform: translateX(4px); +} + +.sidebar-nav li a.active { + color: var(--primary-color); + background-color: rgba(80, 250, 123, 0.15); + border-left: 3px solid var(--primary-color); +} + +.sidebar-nav li a i { + margin-right: 10px; +} + +.sidebar-section { + margin: 15px 0; +} + +.sidebar-section > span { + display: block; + font-weight: bold; + color: var(--text-muted); + margin-bottom: 10px; + margin-top: 15px; + padding: 0 20px; + display: flex; + align-items: center; +} + +.sidebar-section > span i { + margin-right: 10px; +} + +.category-tree { + margin-left: 15px; + padding-left: 10px; + border-left: 1px dashed var(--border-color); +} + +/* Main content area */ +.content { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.content-header { + height: var(--header-height); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + background-color: var(--bg-light); +} + +.header-left, .header-right { + display: flex; + align-items: center; + gap: 15px; +} + +.header-left h2 { + margin-left: 10px; + font-weight: 500; +} + +.content-body { + flex: 1; + overflow-y: auto; + padding: 30px; +} + +/* Subcategories section styling */ +.section-title { + font-size: 1.5rem; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +/* Category grid */ +.category-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +.category-card { + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: var(--bg-light); + padding: 20px; + transition: all 0.3s ease; + height: 100%; + display: flex; + flex-direction: column; +} + +.category-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2); +} + +.category-card a { + display: flex; + flex-direction: column; + color: var(--text-color); + text-decoration: none; + height: 100%; +} + +.category-card i { + font-size: 2.5rem; + margin-bottom: 15px; + color: var(--primary-color); +} + +.category-meta { + margin-top: 15px; + color: var(--text-muted); + font-size: 0.85rem; + display: flex; + flex-direction: column; + gap: 5px; +} + +.add-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + border-style: dashed; + height: 100%; + transition: all 0.3s ease; +} + +.add-card:hover { + background-color: rgba(80, 250, 123, 0.1); + border-color: var(--primary-color); +} + +.add-card i { + color: var(--text-muted); + transition: transform 0.3s ease, color 0.3s ease; +} + +.add-card:hover i { + color: var(--primary-color); + transform: scale(1.2); +} + +.add-card h4 { + color: var(--text-muted); + transition: color 0.3s ease; +} + +.add-card:hover h4 { + color: var(--primary-color); +} + +/* Current Category Indicator */ +.current-category { + background-color: rgba(80, 250, 123, 0.1); + padding: 15px 20px; + margin: -30px -30px 20px -30px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; +} + +.current-category h3 { + font-size: 1.3rem; + margin: 0; + display: flex; + align-items: center; +} + +.current-category h3 i { + margin-right: 10px; + color: var(--primary-color); +} + +.current-category .category-path { + margin-left: auto; + color: var(--text-muted); + font-size: 0.9rem; +} + +.current-category .category-path a { + color: var(--text-muted); + transition: color 0.2s ease; +} + +.current-category .category-path a:hover { + color: var(--primary-color); +} + +/* Document list */ +.document-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +/* Button styles */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + transition: all 0.2s ease; + background-color: var(--bg-color); + border: 1px solid var(--border-color); + margin-left: 8px; +} + +.button i { + margin-right: 8px; +} + +.button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.button.primary { + background-color: var(--primary-color); + color: var(--bg-color); + border: none; +} + +.button.primary:hover { + background-color: rgba(80, 250, 123, 0.8); +} + +.icon-button { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 4px; + background-color: transparent; + transition: all 0.2s ease; +} + +.icon-button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +/* Dashboard */ +.dashboard { + padding: 30px; +} + +.dashboard-section { + margin-bottom: 40px; +} + +.section-header { + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.section-header h3 { + display: flex; + align-items: center; + font-size: 1.4rem; +} + +.section-header h3 i { + margin-right: 12px; + color: var(--primary-color); +} + +/* Document card */ +.document-card { + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: var(--bg-light); + padding: 18px; + display: flex; + justify-content: space-between; + align-items: center; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.document-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.document-info { + flex: 1; +} + +.document-info h4 { + margin-bottom: 8px; + font-size: 1.1rem; +} + +.document-info h4 a { + color: var(--text-color); +} + +.document-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; + color: var(--text-muted); + font-size: 0.85rem; +} + +.document-actions { + display: flex; + gap: 8px; +} + +/* Empty states */ +.empty-state { + padding: 60px; + text-align: center; + border: 1px dashed var(--border-color); + border-radius: 8px; + margin: 40px auto; + max-width: 600px; +} + +.empty-state i { + font-size: 4rem; + color: var(--text-muted); + margin-bottom: 20px; +} + +.empty-state p { + color: var(--text-muted); + font-size: 1.1rem; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background-color: var(--bg-light); + border-radius: 8px; + width: 90%; + max-width: 500px; + overflow: hidden; +} + +.modal-header { + padding: 15px 20px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-body { + padding: 25px; +} + +.close-modal { + font-size: 1.5rem; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + transition: color 0.2s ease; +} + +.close-modal:hover { + color: var(--text-color); +} + +/* Form elements */ +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + color: var(--text-muted); + font-weight: 500; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--bg-color); + color: var(--text-color); + font-family: inherit; + font-size: 1rem; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--accent-color); +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 25px; +} + +.icon-selector { + display: flex; + align-items: center; +} + +.icon-preview { + margin-left: 15px; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; +} + +/* Tag styles */ +.document-tag.small { + font-size: 10px; + padding: 1px 5px; +} + +/* Document view styles */ +.document-view { + padding: 30px; + max-width: 1200px; + margin: 0 auto; +} + +.document-metadata { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 15px; + margin-bottom: 25px; + padding-bottom: 15px; + border-bottom: 1px solid var(--border-color); +} + +/* Editor specific styles */ +.editor-container { + display: flex; + height: calc(100vh - 130px); /* Account for header + document info */ + overflow: hidden; +} + +.editor-pane, .preview-pane { + flex: 1; + height: 100%; + overflow: auto; +} + +.editor-pane { + position: relative; +} + +.preview-pane { + padding: 0; + border-left: 1px solid var(--border-color); + background-color: #fff; +} + +/* Markdown preview enhancements */ +.markdown-body { + box-sizing: border-box; + min-width: 200px; + max-width: 1100px; + margin: 0 auto; + padding: 40px 30px; +} + +.markdown-body blockquote { + padding: 0.5em 1em; + color: #555; + border-left: 0.25em solid #dfe2e5; + background-color: #f7f7f7; + margin: 1em 0; +} + +.markdown-body blockquote[data-type="info"], +.markdown-body blockquote[data-type="note"] { + background-color: #f0f8ff; + border-left-color: var(--info-color); +} + +.markdown-body blockquote[data-type="warning"] { + background-color: #fef9e7; + border-left-color: var(--warning-color); +} + +.markdown-body blockquote[data-type="danger"], +.markdown-body blockquote[data-type="important"] { + background-color: #fff0f0; + border-left-color: var(--danger-color); +} + +.markdown-body blockquote[data-type="caution"] { + background-color: #fdfae5; + border-left-color: var(--caution-color); +} + +.markdown-body blockquote[data-type="tip"] { + background-color: #effaf5; + border-left-color: var(--primary-color); +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body table { + border-collapse: collapse; + margin: 1em 0; + overflow: auto; + width: 100%; +} + +.markdown-body table th, +.markdown-body table td { + border: 1px solid #dfe2e5; + padding: 8px 12px; +} + +.markdown-body table th { + background-color: #f6f8fa; + font-weight: 600; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .sidebar { + position: fixed; + z-index: 100; + transform: translateX(-100%); + } + + .sidebar.open { + transform: translateX(0); + } + + .category-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } +} \ No newline at end of file diff --git a/app/static/fonts/CascadiaCove.ttf b/app/static/fonts/CascadiaCove.ttf new file mode 100644 index 0000000..09e45e8 --- /dev/null +++ b/app/static/fonts/CascadiaCove.ttf @@ -0,0 +1,1817 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page not found · GitHub · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ Skip to content + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+ + + +
+
+ +
+
+ 404 “This is not the web page you are looking for” + + + + + + + + + + + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + diff --git a/app/static/js/app.js b/app/static/js/app.js new file mode 100644 index 0000000..da526bb --- /dev/null +++ b/app/static/js/app.js @@ -0,0 +1,121 @@ +document.addEventListener('DOMContentLoaded', function() { + // Sidebar toggle + const sidebarToggle = document.getElementById('sidebar-toggle'); + const sidebar = document.querySelector('.sidebar'); + + if (sidebarToggle && sidebar) { + sidebarToggle.addEventListener('click', function() { + sidebar.classList.toggle('open'); + }); + } + + // Load category tree + loadCategoryTree(); + + // Initialize keyboard shortcuts + initKeyboardShortcuts(); +}); + +// Load and render category tree in the sidebar +function loadCategoryTree() { + const categoryTree = document.getElementById('category-tree'); + + if (!categoryTree) return; + + fetch('/api/categories') + .then(response => response.json()) + .then(categories => { + renderCategoryTree(categories, categoryTree); + }) + .catch(error => { + console.error('Error loading categories:', error); + }); +} + +// Recursively render category tree +function renderCategoryTree(categories, container) { + if (!categories || categories.length === 0) return; + + categories.forEach(category => { + const li = document.createElement('li'); + + const link = document.createElement('a'); + link.href = `/category/${category.id}`; + link.innerHTML = ` + + ${category.name} + `; + + li.appendChild(link); + + // If this category has children, add a nested ul + if (category.children && category.children.length > 0) { + const ul = document.createElement('ul'); + ul.className = 'category-tree'; + renderCategoryTree(category.children, ul); + li.appendChild(ul); + } + + container.appendChild(li); + }); +} + +// Add keyboard shortcuts +function initKeyboardShortcuts() { + document.addEventListener('keydown', function(e) { + // Only process if not in an input element + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) { + return; + } + + // Ctrl+E to edit current document + if (e.ctrlKey && e.key === 'e') { + e.preventDefault(); + + // Check if on document view page + const path = window.location.pathname; + if (path.match(/^\/document\/\d+$/)) { + // Extract document ID and redirect to edit page + const docId = path.split('/').pop(); + window.location.href = `/document/${docId}/edit`; + } + } + + // 'n' to create new document + if (e.key === 'n' && !e.ctrlKey && !e.altKey && !e.metaKey) { + e.preventDefault(); + window.location.href = '/document/new'; + } + + // '/' to focus search (if we had one) + if (e.key === '/') { + e.preventDefault(); + const searchInput = document.getElementById('search-input'); + if (searchInput) { + searchInput.focus(); + } + } + + // 'g h' sequence for home + if (e.key === 'g') { + const keySequence = function(e) { + if (e.key === 'h') { + window.location.href = '/'; + } + document.removeEventListener('keydown', keySequence); + }; + + document.addEventListener('keydown', keySequence); + } + }); +} + +// Fetch root categories for API +function fetchCategories() { + return fetch('/api/categories') + .then(response => response.json()) + .catch(error => { + console.error('Error fetching categories:', error); + return []; + }); +} \ No newline at end of file diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html new file mode 100644 index 0000000..ac903dd --- /dev/null +++ b/app/templates/auth/login.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} + +{% block title %}Login - Vim Docs{% endblock %} + +{% block auth_content %} +
+
+
+

+ Vim Docs +

+

Sign in to your account

+
+ +
+ {{ form.hidden_tag() }} + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+ +
+ {{ form.username(class="block w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary") }} +
+ {% for error in form.username.errors %} +

{{ error }}

+ {% endfor %} +
+ +
+ +
+ {{ form.password(class="block w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary") }} +
+ {% for error in form.password.errors %} +

{{ error }}

+ {% endfor %} +
+ +
+
+ {{ form.remember_me(class="h-4 w-4 text-primary focus:ring-primary border-gray-600 rounded bg-gray-700") }} + +
+
+ +
+ {{ form.submit(class="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-black bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary") }} +
+ +
+

+ Don't have an account? + + Sign up + +

+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/auth/settings.html b/app/templates/auth/settings.html new file mode 100644 index 0000000..576e341 --- /dev/null +++ b/app/templates/auth/settings.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} + +{% block title %}Settings - Vim Docs{% endblock %} + +{% block header_title %}Settings{% endblock %} + +{% block content %} +
+
+
+

User Settings

+

Customize your Vim Docs experience

+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+ + +
+

Theme Settings

+ +
+
+ +
+ + {{ current_user.theme_color }} +
+
+ +
+

Color Presets

+
+ + + + + + +
+
+
+
+ +
+ +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/auth/signup.html b/app/templates/auth/signup.html new file mode 100644 index 0000000..049716c --- /dev/null +++ b/app/templates/auth/signup.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} + +{% block title %}Sign Up - Vim Docs{% endblock %} + +{% block auth_content %} +
+
+
+

+ Vim Docs +

+

Create your account

+
+ +
+ {{ form.hidden_tag() }} + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+ +
+ {{ form.username(class="block w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary") }} +
+ {% for error in form.username.errors %} +

{{ error }}

+ {% endfor %} +
+ +
+ +
+ {{ form.password(class="block w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary", placeholder="Minimum 8 characters") }} +
+ {% for error in form.password.errors %} +

{{ error }}

+ {% endfor %} +
+ +
+ +
+ {{ form.password2(class="block w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary") }} +
+ {% for error in form.password2.errors %} +

{{ error }}

+ {% endfor %} +
+ +
+ {{ form.submit(class="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-black bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary") }} +
+ +
+

+ Already have an account? + + Sign in + +

+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..4ffd79f --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,332 @@ + + + + + + {% block title %}Vim Docs{% endblock %} + + + + + + + + + + + + {% block extra_css %}{% endblock %} + + + {% if current_user.is_authenticated %} +
+ + + + +
+ +
+
+ +

{% block header_title %}Dashboard{% endblock %}

+ +
+
+ + +
+ +
+
+ +
+ {% block header_actions %}{% endblock %} + +
+ + + +
+
+
+ + +
+ {% block content %}{% endblock %} +
+
+
+ + + + {% else %} +
+ {% block auth_content %}{% endblock %} +
+ {% endif %} + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/app/templates/category.html b/app/templates/category.html new file mode 100644 index 0000000..00586bd --- /dev/null +++ b/app/templates/category.html @@ -0,0 +1,364 @@ +{% extends "base.html" %} + +{% block title %}{{ category.name }} - Vim Docs{% endblock %} + +{% block header_title %} + {{ category.name }} +{% endblock %} + +{% block header_actions %} + + New Document + + +{% endblock %} + +{% block content %} +
+ +
+
+
+ +
+

{{ category.name }}

+
+ +
+ {% if category.description %} + {{ category.description }} + {% else %} + A category for organizing your documents + {% endif %} +
+ +
+
+ + Home + {% if category.parent %} + / + {{ category.parent.name }} + {% endif %} + / + {{ category.name }} +
+
+
+ + + {% if category.children.count() > 0 %} + + {% endif %} + + +
+
+

Documents

+ + + +
+ +
+ {% if category.documents.count() > 0 %} + {% for doc in category.documents %} +
+
+ + +
+ {{ (doc.content[:100] + '...') if doc.content|length > 100 else doc.content }} +
+ +
+
+ {{ doc.updated_date.strftime('%b %d, %Y') }} +
+
+ + {% if doc.tags %} +
+ {% for tag in doc.tags %} + {{ tag.name }} + {% endfor %} +
+ {% endif %} +
+
+ {% endfor %} + {% else %} +
+ +

No documents in this category

+

Create your first document in this category

+ + Create Document + +
+ {% endif %} +
+
+
+ + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/document_edit.html b/app/templates/document_edit.html new file mode 100644 index 0000000..dac0651 --- /dev/null +++ b/app/templates/document_edit.html @@ -0,0 +1,491 @@ +{% extends "base.html" %} + +{% block title %}{% if document %}Edit: {{ document.title }}{% else %}New Document{% endif %} - Vim Docs{% endblock %} + +{% block header_title %}{% if document %}Edit: {{ document.title }}{% else %}New Document{% endif %}{% endblock %} + +{% block header_actions %} + +{% if document and document.id %} + + Export + +{% endif %} + +{% endblock %} + +{% block extra_css %} + + + + + + + + + + +{% endblock %} + +{% block content %} + +
+
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ {% if document and document.tags %} + {% for tag in document.tags %} + + {{ tag.name }} + × + + {% endfor %} + {% endif %} +
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ + +
+ + Document saved successfully! +
+{% endblock %} + +{% block extra_js %} + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/document_view.html b/app/templates/document_view.html new file mode 100644 index 0000000..1dbcd7b --- /dev/null +++ b/app/templates/document_view.html @@ -0,0 +1,278 @@ +{% extends "base.html" %} + +{% block title %}{{ document.title }} - Vim Docs{% endblock %} + +{% block header_title %}{{ document.title }}{% endblock %} + +{% block header_actions %} + + Edit + + + Export + +{% endblock %} + +{% block extra_css %} + + + + +{% endblock %} + +{% block content %} +
+ + +
+ +
+
+{% endblock %} + +{% block extra_js %} + + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..f63fc17 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,400 @@ +{% extends "base.html" %} + +{% block title %}Dashboard - Vim Docs{% endblock %} + +{% block header_title %}Dashboard{% endblock %} + +{% block content %} +
+ +
+
+
+
+

Welcome back, {{ current_user.username }}!

+

Manage your Vim and coding documentation with ease.

+
+ + New Document + + +
+
+ +
+
+
+ + +
+
+

Recent Documents

+ + View All + +
+ +
+ {% if recent_docs %} + {% for doc in recent_docs %} +
+
+ + +
+ {{ (doc.content[:100] + '...') if doc.content|length > 100 else doc.content }} +
+ +
+
+ {{ doc.updated_date.strftime('%b %d, %Y') }} +
+ + {% if doc.category %} +
+ {{ doc.category.name }} +
+ {% endif %} +
+ + {% if doc.tags %} +
+ {% for tag in doc.tags %} + {{ tag.name }} + {% endfor %} +
+ {% endif %} +
+
+ {% endfor %} + {% else %} +
+ +

No documents yet

+

Create your first document to get started

+ + Create Document + +
+ {% endif %} +
+
+ + +
+
+

My Categories

+
+ +
+ {% if categories %} + {% for category in categories %} + + {% endfor %} + + +
+
+
+ +
+

New Category

+

Create a new category to organize your docs

+
+
+ {% else %} +
+ +

No categories yet

+

Organize your documents by creating categories

+ +
+ {% endif %} +
+
+
+ + + + + + +{% endblock %} \ No newline at end of file diff --git a/create_db.py b/create_db.py new file mode 100644 index 0000000..3ea99db --- /dev/null +++ b/create_db.py @@ -0,0 +1,117 @@ +from app import app, db +from app.models.user import User +from app.models.document import Document, Category, Tag +from werkzeug.security import generate_password_hash +import os + +def setup_database(): + """Set up the database tables and create a demo user if none exists.""" + with app.app_context(): + # Create all tables + db.create_all() + + # Check if any users exist + if User.query.count() == 0: + print('Creating demo user...') + + # Create demo user + demo_user = User(username='demo') + demo_user.set_password('password') + db.session.add(demo_user) + db.session.flush() # To get the user ID + + # Create a root category for the demo user + root_category = Category( + name='My Documents', + icon='mdi-folder', + description='Default document category', + user_id=demo_user.id, + is_root=True + ) + db.session.add(root_category) + + # Create some sample categories + categories = [ + Category( + name='Vim Commands', + icon='mdi-vim', + user_id=demo_user.id, + description='Essential Vim commands and shortcuts' + ), + Category( + name='Flask Development', + icon='mdi-flask', + user_id=demo_user.id, + description='Flask web development notes' + ), + Category( + name='Python Snippets', + icon='mdi-language-python', + user_id=demo_user.id, + description='Useful Python code snippets' + ) + ] + + for category in categories: + db.session.add(category) + + # Create a sample document + sample_doc = Document( + title='Getting Started with Vim', + content="""# Getting Started with Vim + +## Basic Commands + +### Movement +- `h` - move left +- `j` - move down +- `k` - move up +- `l` - move right + +### Modes +- `i` - enter insert mode +- `Esc` - return to normal mode +- `v` - enter visual mode +- `:` - enter command mode + +> Vim has a steep learning curve, but it's worth it! + +> [!TIP] +> Use `vimtutor` to learn Vim basics interactively. + +> [!NOTE] +> Vim is available on almost all Unix-like systems. +""", + user_id=demo_user.id, + category_id=categories[0].id + ) + db.session.add(sample_doc) + + # Create some tags + tags = [ + Tag(name='vim', user_id=demo_user.id, color='#50fa7b'), + Tag(name='editor', user_id=demo_user.id, color='#bd93f9'), + Tag(name='tutorial', user_id=demo_user.id, color='#ff79c6') + ] + + for tag in tags: + db.session.add(tag) + + # Associate tags with the document + sample_doc.tags = tags + + # Commit all changes + db.session.commit() + + print('Demo user and sample data created successfully!') + else: + print('Database already contains users, skipping demo data creation.') + +if __name__ == '__main__': + # Create database file if it doesn't exist + db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'app', 'docs.db') + if not os.path.exists(db_path): + print(f'Creating database file at {db_path}') + + setup_database() + print('Database setup complete!') \ No newline at end of file diff --git a/instance/docs.db b/instance/docs.db new file mode 100644 index 0000000000000000000000000000000000000000..a77b8dfefb976ccbf30b8d0f1c28386b5a45f0ce GIT binary patch literal 36864 zcmeI(&2HjW6bEoSWCBJ_sT4&lMv-pRi2~A)Vq^1>P0J*n8Cn8`kTlap(ZwH$Tfh`d zCs8$0leB%3?q}6!=o9oky6m#|Iza3cX1eGK{Vl-e+`{;8 zgpUMK6h0$F5Cmy*XET3VV~?woWj+%F%ZnBzVei-U`ue31limu6y$}Cf|LKFqdL#aq z^fvzcqCtp500Izz00bZa0SL?lZX(fWa(7p})jitjTBd7^dsg3Td-Qx1dJ;Qm>gASB zTIFX|orIc5DlW|ej!7!Dmi|I-l3Kk*YA4m|Qxcch`q~SczHC;G%FS2g3;h*I1=jPF z+zm?e@z3i`z4D^QI|cisNmGBWH}%?qemr|oB;}a@8Z=o~us;j=UE}FeY%O`TD+*5E zv@S>AciAV{HXVDe|GYhWO>DrZEr*^;H{Yy9lUrNjFNqM~v{Q608y8!k;W=K{BB$l% z!OL)U(XC$sWle%T%j!q;t9PfQRNZ7%Y754zB`v z@%xXgVIUORNGMfvkOQj>Wo8=U)#)f!HePEUYc6$_J^%J>J zv`>5YpxOh^$;Ri_tl@UuVIUQLP0b@XsMn5L%`&Td!rwS;f2CuCoYX3f6P={^D|kw# z`}we(r}1EE^4MhmcOI=JOB;)7lW$ID5Byvz#^2R9&4^!*Kfm_5rt;JIY6xx`eMdcp z^yG)-=&*X63Gr6GbEY*ihR(oq0&TpIdE0;v`Qhch_}^D@&Zs?fU5`Ag*QenuX4B=wqK z1SG|W@S*AJx)@DvZi=^}pDiBe_mxnVc;p=!nyze?hrAj-W(??P^x7So?F%}(nDvsC zVz}GFBZzll^{n=PdN;(=>#PRF#3ur~VS)ezAOHafKmY;|fB*y_009U<;7;JWCPszL zn@0KZ!1<1{?@g`--hNIgTC_?fgEeTGteTNkqGb1#+* zrxbFUtbVLzZCdEW*?x(MzXf)~1OW&@00Izz00bZa0SG_<0uX?}k_kj3n-c#Dga7^? zNxTsfejmyJCT@>%xp